diff --git a/Editor/Localization/ja.po b/Editor/Localization/ja.po index 8f32e3b..6da4df1 100644 --- a/Editor/Localization/ja.po +++ b/Editor/Localization/ja.po @@ -500,3 +500,27 @@ msgstr "@hereをメッセージにつける" # addHereInSlackMessage tooltip msgid "Whether adding @here into Slack messages or not" msgstr "Slack通知メッセージに@hereを付けます" + +# withScreenshotOnError +msgid "Take screenshot" +msgstr "スクリーンショット" + +# withScreenshotOnError tooltip +msgid "Take a screenshot when posting an error terminated report" +msgstr "エラー終了時にスクリーンショットを撮影します" + +# postOnNormally +msgid "Normally terminated report" +msgstr "正常終了時にもレポート" + +# postOnNormally tooltip +msgid "Post a report if normally terminates." +msgstr "正常終了時にもレポートをポストします" + +# withScreenshotOnNormally (same as withScreenshotOnError) +msgid "Take screenshot" +msgstr "スクリーンショット" + +# withScreenshotOnNormally tooltip +msgid "Take a screenshot when posting a normally terminated report" +msgstr "正常終了時にスクリーンショットを撮影します" diff --git a/Editor/UI/Reporters/SlackReporterEditor.cs b/Editor/UI/Reporters/SlackReporterEditor.cs index 0b5dbf4..43870b1 100644 --- a/Editor/UI/Reporters/SlackReporterEditor.cs +++ b/Editor/UI/Reporters/SlackReporterEditor.cs @@ -14,8 +14,7 @@ namespace DeNA.Anjin.Editor.UI.Reporters public class SlackReporterEditor : UnityEditor.Editor { private const float SpacerPixels = 10f; - private const float SpacerPixelsUnderHeader = 4f; - + private static readonly string s_description = L10n.Tr("Description"); private static readonly string s_descriptionTooltip = L10n.Tr("Description about this Reporter instance"); private SerializedProperty _descriptionProp; @@ -31,8 +30,6 @@ public class SlackReporterEditor : UnityEditor.Editor private SerializedProperty _slackChannelsProp; private GUIContent _slackChannelsGUIContent; - private static readonly string s_slackMentionSettingsHeader = L10n.Tr("Slack Mention Settings"); - private static readonly string s_mentionSubTeamIDs = L10n.Tr("Sub Team IDs to Mention"); private static readonly string s_mentionSubTeamIDsTooltip = L10n.Tr("Sub team IDs to mention (comma separates)"); private SerializedProperty _mentionSubTeamIDsProp; @@ -43,6 +40,20 @@ public class SlackReporterEditor : UnityEditor.Editor private SerializedProperty _addHereInSlackMessageProp; private GUIContent _addHereInSlackMessageGUIContent; + private static readonly string s_withScreenshotOnError = L10n.Tr("Take screenshot"); + private static readonly string s_withScreenshotOnErrorTooltip = L10n.Tr("Take a screenshot when posting an error terminated report"); + private SerializedProperty _withScreenshotOnErrorProp; + private GUIContent _withScreenshotOnErrorGUIContent; + + private static readonly string s_postOnNormally = L10n.Tr("Normally terminated report"); + private static readonly string s_postOnNormallyTooltip = L10n.Tr("Post a report if normally terminates."); + private SerializedProperty _postOnNormallyProp; + private GUIContent _postOnNormallyGUIContent; + + private static readonly string s_withScreenshotOnNormally = L10n.Tr("Take screenshot"); + private static readonly string s_withScreenshotOnNormallyTooltip = L10n.Tr("Take a screenshot when posting a normally terminated report"); + private SerializedProperty _withScreenshotOnNormallyProp; + private GUIContent _withScreenshotOnNormallyGUIContent; private void OnEnable() { @@ -66,13 +77,22 @@ private void Initialize() _addHereInSlackMessageProp = serializedObject.FindProperty(nameof(SlackReporter.addHereInSlackMessage)); _addHereInSlackMessageGUIContent = new GUIContent(s_addHereInSlackMessage, s_addHereInSlackMessageTooltip); + + _withScreenshotOnErrorProp = serializedObject.FindProperty(nameof(SlackReporter.withScreenshotOnError)); + _withScreenshotOnErrorGUIContent = new GUIContent(s_withScreenshotOnError, s_withScreenshotOnErrorTooltip); + + _postOnNormallyProp = serializedObject.FindProperty(nameof(SlackReporter.postOnNormally)); + _postOnNormallyGUIContent = new GUIContent(s_postOnNormally, s_postOnNormallyTooltip); + + _withScreenshotOnNormallyProp = serializedObject.FindProperty(nameof(SlackReporter.withScreenshotOnNormally)); + _withScreenshotOnNormallyGUIContent = new GUIContent(s_withScreenshotOnNormally, s_withScreenshotOnNormallyTooltip); } public override void OnInspectorGUI() { serializedObject.Update(); - + EditorGUILayout.PropertyField(_descriptionProp, _descriptionGUIContent); GUILayout.Space(SpacerPixels); @@ -80,12 +100,19 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(_slackChannelsProp, _slackChannelsGUIContent); GUILayout.Space(SpacerPixels); - GUILayout.Label(s_slackMentionSettingsHeader); - GUILayout.Space(SpacerPixelsUnderHeader); - EditorGUILayout.PropertyField(_mentionSubTeamIDsProp, _mentionSubTeamIDsGUIContent); EditorGUILayout.PropertyField(_addHereInSlackMessageProp, _addHereInSlackMessageGUIContent); + GUILayout.Space(SpacerPixels); + EditorGUILayout.PropertyField(_withScreenshotOnErrorProp, _withScreenshotOnErrorGUIContent); + + GUILayout.Space(SpacerPixels); + EditorGUILayout.PropertyField(_postOnNormallyProp, _postOnNormallyGUIContent); + + EditorGUI.BeginDisabledGroup(!_postOnNormallyProp.boolValue); + EditorGUILayout.PropertyField(_withScreenshotOnNormallyProp, _withScreenshotOnNormallyGUIContent); + EditorGUI.EndDisabledGroup(); + serializedObject.ApplyModifiedProperties(); } } diff --git a/Editor/UI/Settings/AutopilotSettingsEditor.cs b/Editor/UI/Settings/AutopilotSettingsEditor.cs index 4a5b7b0..b990dc4 100644 --- a/Editor/UI/Settings/AutopilotSettingsEditor.cs +++ b/Editor/UI/Settings/AutopilotSettingsEditor.cs @@ -146,7 +146,7 @@ internal void Stop() var autopilot = FindObjectOfType(); if (autopilot) { - autopilot.TerminateAsync(ExitCode.Normally).Forget(); + autopilot.TerminateAsync(ExitCode.Normally, reporting: false).Forget(); } } diff --git a/README.md b/README.md index d9b6d71..48d4d26 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ This item can also be overridden from the commandline (see below).
Time Scale
Time.timeScale. Default is 1.0
JUnit Report Path
Specifies the JUnit format report file output path (optional). If there are zero errors and zero failures, the autopilot run is considered to have completed successfully.
Logger
Logger used for this autopilot settings. If omitted, Debug.unityLogger will be used as default.
-
Reporter
Reporter that called when some errors occurred in target application
+
Reporter
Reporter to be called on Autopilot terminate.
#### Error Handling Settings @@ -461,12 +461,15 @@ The instance of this Reporter (.asset file) can have the following settings. The bot must be invited to the channel.
Mention Sub Team IDs
Comma Separated Team IDs to mention in notification message
Add Here In Slack Message
Add @here to notification message. Default is off
+
Take screenshot
Take a screenshot when posting an error terminated report. Default is on
+
Normally terminated report
Post a report if normally terminates. Default is off
+
Take screenshot
Take a screenshot when posting a normally terminated report. Default is off
-You can create a bot on the following page: +You can create a Slack Bot on the following page: [Slack API: Applications](https://api.slack.com/apps) -The bot needs the following permissions: +The Slack Bot needs the following permissions: - chat:write - files:write diff --git a/README_ja.md b/README_ja.md index 3057db7..09734de 100644 --- a/README_ja.md +++ b/README_ja.md @@ -119,7 +119,7 @@ v1.0.0時点では `EmergencyExitAgent` の使用を想定しています。
Time Scale
Time.timeScaleを指定します。デフォルトは1.0
JUnit Report Path
JUnit形式のレポートファイル出力パスを指定します(省略可)。オートパイロット実行の成否は、Unityエディターの終了コードでなくこのファイルを見て判断するのが確実です。errors, failuresともに0件であれば正常終了と判断できます。
Logger
オートパイロットが使用するLogger指定します。省略時は Debug.unityLogger がデフォルトとして使用されます
-
Reporter
対象のアプリケーションで発生したエラーを通知するReporterを指定します
+
Reporter
オートパイロット終了時に通知を行なうReporterを指定します
#### エラーハンドリング設定 @@ -464,13 +464,16 @@ Slackにレポート送信するReporterです。 コマンドライン引数 -SLACK_CHANNELS で上書きできます。 チャンネルにはBotを招待しておく必要があります。
Mention Sub Team IDs
通知メッセージでメンションするチームのIDをカンマ区切りで指定します
-
Add Here In Slack Message
通知メッセージに@hereを付けます。デフォルトはoff
+
Add Here In Slack Message
通知メッセージに@hereを付けます(デフォルト: off)
+
Take screenshot
エラー終了時にスクリーンショットを撮影します(デフォルト: on)
+
Normally terminated report
正常終了時にもレポートをポストします(デフォルト: off)
+
Take screenshot
正常終了時にスクリーンショットを撮影します(デフォルト: off)
-Botは次のページで作成できます。 +Slack Botは次のページで作成できます。 [Slack API: Applications](https://api.slack.com/apps) -Botには次の権限が必要です。 +Slack Botには次の権限が必要です。 - chat:write - files:write diff --git a/Runtime/Autopilot.cs b/Runtime/Autopilot.cs index e41bfc7..fd4e87b 100644 --- a/Runtime/Autopilot.cs +++ b/Runtime/Autopilot.cs @@ -15,10 +15,28 @@ namespace DeNA.Anjin { + /// + /// Can be terminated. + /// + public interface ITerminatable + { + /// + /// Terminate autopilot + /// + /// Exit code for Unity Editor/ Player-build + /// Log message string or terminate message + /// Stack trace when terminate by the log message + /// Call Reporter if true + /// Cancellation token + /// A task awaits termination get completed + UniTask TerminateAsync(ExitCode exitCode, string message = null, string stackTrace = null, + bool reporting = true, CancellationToken token = default); + } + /// /// Autopilot main logic /// - public class Autopilot : MonoBehaviour + public class Autopilot : MonoBehaviour, ITerminatable { private AbstractLoggerAsset _loggerAsset; private ILogger _logger; @@ -28,6 +46,7 @@ public class Autopilot : MonoBehaviour private AutopilotState _state; private AutopilotSettings _settings; private float _startTime; + private bool _isTerminating; private void Start() { @@ -51,7 +70,7 @@ private void Start() // 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, _settings.reporter); + _logMessageHandler = new LogMessageHandler(_settings, this); _dispatcher = new AgentDispatcher(_settings, _logger, _randomFactory); var dispatched = _dispatcher.DispatchByScene(SceneManager.GetActiveScene(), false); @@ -126,7 +145,8 @@ internal static void ConvertSlackReporterFromObsoleteSlackSettings(AutopilotSett private IEnumerator Lifespan(int timeoutSec) { yield return new WaitForSecondsRealtime(timeoutSec); - yield return UniTask.ToCoroutine(() => TerminateAsync(ExitCode.Normally)); + yield return UniTask.ToCoroutine(() => + TerminateAsync(ExitCode.Normally, "Autopilot has reached the end of its lifespan.")); } private void OnDestroy() @@ -134,26 +154,32 @@ private void OnDestroy() // Clear event listeners. // When play mode is stopped by the user, onDestroy calls without TerminateAsync. + _logger?.Log("Destroy Autopilot object"); _dispatcher?.Dispose(); _logMessageHandler?.Dispose(); _settings.loggerAsset?.Dispose(); } - /// - /// Terminate autopilot - /// - /// Exit code for Unity Editor - /// Log message string when terminate by the log message - /// Stack trace when terminate by the log message - /// Cancellation token - /// 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 message = null, string stackTrace = null, + bool reporting = true, CancellationToken token = default) { + if (_isTerminating) + { + return; // Prevent multiple termination. + } + + _isTerminating = true; + + if (reporting && _state.IsRunning && _settings.reporter != null) + { + await _settings.reporter.PostReportAsync(message, stackTrace, exitCode, token); + } + if (_state.settings != null && !string.IsNullOrEmpty(_state.settings.junitReportPath)) { var time = Time.realtimeSinceStartup - _startTime; - JUnitReporter.Output(_state.settings.junitReportPath, (int)exitCode, logString, stackTrace, time); + JUnitReporter.Output(_state.settings.junitReportPath, (int)exitCode, message, stackTrace, time); } DestroyImmediate(this.gameObject); @@ -162,12 +188,6 @@ public async UniTask TerminateAsync(ExitCode exitCode, string logString = null, await Launcher.TeardownLaunchAutopilotAsync(_state, _logger, exitCode, "Autopilot", token); } - /// - /// 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) { diff --git a/Runtime/Reporters/AbstractReporter.cs b/Runtime/Reporters/AbstractReporter.cs index 19e28b6..1454ea5 100644 --- a/Runtime/Reporters/AbstractReporter.cs +++ b/Runtime/Reporters/AbstractReporter.cs @@ -8,7 +8,8 @@ namespace DeNA.Anjin.Reporters { /// - /// Reporter base class + /// Reporter base class. + /// Reporter is called on Autopilot termination. /// public abstract class AbstractReporter : ScriptableObject { @@ -22,17 +23,15 @@ public abstract class AbstractReporter : ScriptableObject /// /// Post report log message, stacktrace and screenshot /// - /// Log message - /// Stack trace - /// Log message type - /// With screenshot + /// Log message or terminate message + /// Stack trace (can be null) + /// Exit code indicating the reason for termination /// Cancellation token /// public abstract UniTask PostReportAsync( - string logString, + string message, string stackTrace, - LogType type, - bool withScreenshot, + ExitCode exitCode, CancellationToken cancellationToken = default ); } diff --git a/Runtime/Reporters/CompositeReporter.cs b/Runtime/Reporters/CompositeReporter.cs index 18a8f92..de76e8a 100644 --- a/Runtime/Reporters/CompositeReporter.cs +++ b/Runtime/Reporters/CompositeReporter.cs @@ -23,10 +23,9 @@ public class CompositeReporter : AbstractReporter /// public override async UniTask PostReportAsync( - string logString, + string message, string stackTrace, - LogType type, - bool withScreenshot, + ExitCode exitCode, CancellationToken cancellationToken = default ) { @@ -34,7 +33,7 @@ await UniTask.WhenAll( reporters .Where(r => r != this && r != null) .Select( - r => r.PostReportAsync(logString, stackTrace, type, withScreenshot, cancellationToken) + r => r.PostReportAsync(message, stackTrace, exitCode, cancellationToken) ) ); } diff --git a/Runtime/Reporters/SlackReporter.cs b/Runtime/Reporters/SlackReporter.cs index a38c8b3..4cc8997 100644 --- a/Runtime/Reporters/SlackReporter.cs +++ b/Runtime/Reporters/SlackReporter.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.Threading; @@ -34,18 +34,41 @@ public class SlackReporter : AbstractReporter /// public bool addHereInSlackMessage; + /// + /// With take a screenshot or not (on error terminates). + /// + public bool withScreenshotOnError = true; + + /// + /// Post a report if normally terminates. + /// + public bool postOnNormally; + + /// + /// With take a screenshot or not (on normally terminates). + /// + public bool withScreenshotOnNormally; + private readonly ISlackMessageSender _sender = new SlackMessageSender(new SlackAPI()); /// public override async UniTask PostReportAsync( - string logString, + string message, string stackTrace, - LogType type, - bool withScreenshot, + ExitCode exitCode, CancellationToken cancellationToken = default ) { + if (exitCode == ExitCode.Normally && !postOnNormally) + { + return; + } + + var withScreenshot = exitCode == ExitCode.Normally ? withScreenshotOnNormally : withScreenshotOnError; + // TODO: build message body with template and placeholders + OverwriteByCommandlineArguments(); + // TODO: log warn if slackToken or slackChannels is empty // NOTE: In _sender.send, switch the execution thread to the main thread, so UniTask.WhenAll is meaningless. foreach (var slackChannel in slackChannels.Split(',')) @@ -55,19 +78,31 @@ public override async UniTask PostReportAsync( return; } - await _sender.Send( - slackToken, - slackChannel, - mentionSubTeamIDs.Split(','), - addHereInSlackMessage, - logString, - stackTrace, - withScreenshot, - cancellationToken - ); + await PostReportAsync(slackChannel, message, stackTrace, withScreenshot, cancellationToken); } } + private async UniTask PostReportAsync( + string slackChannel, + string message, + string stackTrace, + bool withScreenshot, + CancellationToken cancellationToken = default + ) + { + await _sender.Send( + slackToken, + slackChannel, + mentionSubTeamIDs.Split(','), + addHereInSlackMessage, + message, + stackTrace, + withScreenshot, + cancellationToken + ); + // TODO: can log slack post url? + } + private void OverwriteByCommandlineArguments() { var args = new Arguments(); diff --git a/Runtime/Utilities/LogMessageHandler.cs b/Runtime/Utilities/LogMessageHandler.cs index b08a3b7..b87992f 100644 --- a/Runtime/Utilities/LogMessageHandler.cs +++ b/Runtime/Utilities/LogMessageHandler.cs @@ -8,7 +8,6 @@ using DeNA.Anjin.Reporters; using DeNA.Anjin.Settings; using UnityEngine; -using Object = UnityEngine.Object; namespace DeNA.Anjin.Utilities { @@ -18,18 +17,19 @@ namespace DeNA.Anjin.Utilities public class LogMessageHandler : IDisposable { private readonly AutopilotSettings _settings; - private readonly AbstractReporter _reporter; + private readonly ITerminatable _autopilot; + private List _ignoreMessagesRegexes; /// /// Constructor /// /// Autopilot settings - /// Reporter implementation - public LogMessageHandler(AutopilotSettings settings, AbstractReporter reporter = null) + /// Autopilot instance that termination target + public LogMessageHandler(AutopilotSettings settings, ITerminatable autopilot) { _settings = settings; - _reporter = reporter != null ? reporter : _settings.reporter; + _autopilot = autopilot; Application.logMessageReceivedThreaded += this.HandleLog; } @@ -65,22 +65,13 @@ public async void HandleLog(string logString, string stackTrace, LogType type) _settings.loggerAsset.Logger.Log(type, logString, stackTrace); } - if (_reporter != null) + if (type == LogType.Exception) { - await _reporter.PostReportAsync(logString, stackTrace, type, true); + await _autopilot.TerminateAsync(ExitCode.UnCatchExceptions, logString, stackTrace); } - - var autopilot = Object.FindObjectOfType(); - if (autopilot != null) + else { - if (type == LogType.Exception) - { - await autopilot.TerminateAsync(ExitCode.UnCatchExceptions, logString, stackTrace); - } - else - { - await autopilot.TerminateAsync(ExitCode.AutopilotFailed, logString, stackTrace); - } + await _autopilot.TerminateAsync(ExitCode.AutopilotFailed, logString, stackTrace); } } diff --git a/Tests/Runtime/AutopilotTest.cs b/Tests/Runtime/AutopilotTest.cs index bd7e775..78bc982 100644 --- a/Tests/Runtime/AutopilotTest.cs +++ b/Tests/Runtime/AutopilotTest.cs @@ -53,7 +53,7 @@ public async Task TerminateAsync_Normally_DestroyedAutopilotAndAgentObjects() var agents = Object.FindObjectsOfType(); Assume.That(agents, Is.Not.Empty, "Agents are running"); - await autopilot.TerminateAsync(ExitCode.Normally); + await autopilot.TerminateAsync(ExitCode.Normally, reporting: false); await UniTask.NextFrame(); // wait for destroy autopilot = Object.FindObjectOfType(); // re-find after terminated diff --git a/Tests/Runtime/TestDoubles/SpyReporter.cs b/Tests/Runtime/TestDoubles/SpyReporter.cs index d69c14b..a4f242d 100644 --- a/Tests/Runtime/TestDoubles/SpyReporter.cs +++ b/Tests/Runtime/TestDoubles/SpyReporter.cs @@ -5,7 +5,6 @@ using System.Threading; using Cysharp.Threading.Tasks; using DeNA.Anjin.Reporters; -using DeNA.Anjin.Settings; using UnityEngine; namespace DeNA.Anjin.TestDoubles @@ -13,26 +12,22 @@ namespace DeNA.Anjin.TestDoubles /// /// A spy for /// - // [CreateAssetMenu(fileName = "New SpyReporter", menuName = "Anjin/Spy Reporter", order = 34)] + // [CreateAssetMenu(fileName = "New SpyReporter", menuName = "Anjin/Spy Reporter", order = 55)] public class SpyReporter : AbstractReporter { public List> Arguments { get; } = new List>(); - + public override async UniTask PostReportAsync( - string logString, + string message, string stackTrace, - LogType type, - bool withScreenshot, + ExitCode exitCode, CancellationToken cancellationToken = default ) { Debug.Log("Reporter called"); Arguments.Add(new Dictionary { - {"logString", logString}, - {"stackTrace", stackTrace}, - {"type", type.ToString()}, - {"withScreenshot", withScreenshot.ToString()} + { "message", message }, { "stackTrace", stackTrace }, { "exitCode", exitCode.ToString() } }); await UniTask.NextFrame(cancellationToken); } diff --git a/Tests/Runtime/TestDoubles/SpyTerminatable.cs b/Tests/Runtime/TestDoubles/SpyTerminatable.cs new file mode 100644 index 0000000..b26fa19 --- /dev/null +++ b/Tests/Runtime/TestDoubles/SpyTerminatable.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2023-2024 DeNA Co., Ltd. +// This software is released under the MIT License. + +using System.Threading; +using Cysharp.Threading.Tasks; + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +namespace DeNA.Anjin.TestDoubles +{ + public class SpyTerminatable : ITerminatable + { + public bool IsCalled { get; private set; } = false; + public ExitCode CapturedExitCode { get; private set; } + public string CapturedMessage { get; private set; } + public string CapturedStackTrace { get; private set; } + public bool CapturedReporting { get; private set; } + + public async UniTask TerminateAsync(ExitCode exitCode, string message = null, string stackTrace = null, + bool reporting = true, CancellationToken token = default) + { + IsCalled = true; + CapturedExitCode = exitCode; + CapturedMessage = message; + CapturedStackTrace = stackTrace; + CapturedReporting = reporting; + } + } +} diff --git a/Tests/Runtime/TestDoubles/SpyTerminatable.cs.meta b/Tests/Runtime/TestDoubles/SpyTerminatable.cs.meta new file mode 100644 index 0000000..2519f0f --- /dev/null +++ b/Tests/Runtime/TestDoubles/SpyTerminatable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 337802fde88143dda4d8739684434e39 +timeCreated: 1730381729 \ No newline at end of file diff --git a/Tests/Runtime/Utilities/LogMessageHandlerTest.cs b/Tests/Runtime/Utilities/LogMessageHandlerTest.cs index 71a8342..69b42e0 100644 --- a/Tests/Runtime/Utilities/LogMessageHandlerTest.cs +++ b/Tests/Runtime/Utilities/LogMessageHandlerTest.cs @@ -14,211 +14,189 @@ namespace DeNA.Anjin.Utilities [TestFixture] public class LogMessageHandlerTest { - [Test] - public async Task HandleLog_LogTypeIsLog_notReported() + private AutopilotSettings _settings; + private SpyTerminatable _spyTerminatable; + private LogMessageHandler _sut; + + private const string Message = "message"; + private const string StackTrace = "stack trace"; + + [SetUp] + public void SetUp() { - var settings = ScriptableObject.CreateInstance(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - sut.HandleLog(string.Empty, string.Empty, LogType.Log); - await UniTask.NextFrame(); + _settings = ScriptableObject.CreateInstance(); + _settings.lifespanSec = 5; + _settings.ignoreMessages = new string[] { }; - Assert.That(spyReporter.Arguments, Is.Empty); + _spyTerminatable = new SpyTerminatable(); + _sut = new LogMessageHandler(_settings, _spyTerminatable); } - [Test] - public async Task HandleLog_LogTypeExceptionHandle_reported() + [TearDown] + public void TearDown() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); + _sut.Dispose(); + } - settings.handleException = true; - sut.HandleLog(string.Empty, string.Empty, LogType.Exception); + [Test] + public async Task HandleLog_LogTypeIsLog_TerminateIsNotCalled() + { + _sut.HandleLog(Message, StackTrace, LogType.Log); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Not.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.False); } [Test] - public async Task HandleLog_LogTypeExceptionNotHandle_notReported() + public async Task HandleLog_LogTypeExceptionHandle_TerminateIsCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.handleException = false; - sut.HandleLog(string.Empty, string.Empty, LogType.Exception); + _settings.handleException = true; + _sut.HandleLog(Message, StackTrace, LogType.Exception); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.True); + Assert.That(_spyTerminatable.CapturedExitCode, Is.EqualTo(ExitCode.UnCatchExceptions)); + 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_LogTypeAssertHandle_reported() + public async Task HandleLog_LogTypeExceptionNotHandle_TerminateIsNotCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.handleAssert = true; - sut.HandleLog(string.Empty, string.Empty, LogType.Assert); + _settings.handleException = false; + _sut.HandleLog(Message, StackTrace, LogType.Exception); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Not.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.False); } [Test] - public async Task HandleLog_LogTypeAssertNotHandle_notReported() + public async Task HandleLog_LogTypeAssertHandle_TerminateIsCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.handleAssert = false; - sut.HandleLog(string.Empty, string.Empty, LogType.Assert); + _settings.handleAssert = true; + _sut.HandleLog(Message, StackTrace, LogType.Assert); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.True); + Assert.That(_spyTerminatable.CapturedExitCode, Is.EqualTo(ExitCode.AutopilotFailed)); + 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_LogTypeErrorHandle_reported() + public async Task HandleLog_LogTypeAssertNotHandle_TerminateIsNotCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.handleError = true; - sut.HandleLog(string.Empty, string.Empty, LogType.Error); + _settings.handleAssert = false; + _sut.HandleLog(Message, StackTrace, LogType.Assert); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Not.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.False); } [Test] - public async Task HandleLog_LogTypeErrorNotHandle_notReported() + public async Task HandleLog_LogTypeErrorHandle_TerminateIsCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.handleError = false; - sut.HandleLog(string.Empty, string.Empty, LogType.Error); + _settings.handleError = true; + _sut.HandleLog(Message, StackTrace, LogType.Error); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.True); + Assert.That(_spyTerminatable.CapturedExitCode, Is.EqualTo(ExitCode.AutopilotFailed)); + 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_LogTypeWarningHandle_reported() + public async Task HandleLog_LogTypeErrorNotHandle_TerminateIsNotCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.handleWarning = true; - sut.HandleLog(string.Empty, string.Empty, LogType.Warning); + _settings.handleError = false; + _sut.HandleLog(Message, StackTrace, LogType.Error); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Not.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.False); } [Test] - public async Task HandleLog_LogTypeWarningNotHandle_notReported() + public async Task HandleLog_LogTypeWarningHandle_TerminateIsCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.handleWarning = false; - sut.HandleLog(string.Empty, string.Empty, LogType.Warning); + _settings.handleWarning = true; + _sut.HandleLog(Message, StackTrace, LogType.Warning); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.True); + Assert.That(_spyTerminatable.CapturedExitCode, Is.EqualTo(ExitCode.AutopilotFailed)); + 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_notReported() + public async Task HandleLog_LogTypeWarningNotHandle_TerminateIsNotCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.ignoreMessages = new[] { "ignore" }; - settings.handleException = true; - sut.HandleLog("xxx_ignore_xxx", string.Empty, LogType.Exception); + _settings.handleWarning = false; + _sut.HandleLog(Message, StackTrace, LogType.Warning); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.False); } [Test] - public async Task HandleLog_MatchIgnoreMessagePattern_notReported() + public async Task HandleLog_ContainsIgnoreMessage_TerminateIsNotCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.ignoreMessages = new[] { "ignore.+ignore" }; - settings.handleException = true; - sut.HandleLog("ignore_xxx_ignore", string.Empty, LogType.Exception); + _settings.ignoreMessages = new[] { "ignore" }; + _settings.handleException = true; + _sut.HandleLog("xxx_ignore_xxx", string.Empty, LogType.Exception); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.False); } [Test] - public async Task HandleLog_NotMatchIgnoreMessagePattern_Reported() + public async Task HandleLog_MatchIgnoreMessagePattern_TerminateIsNotCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.ignoreMessages = new[] { "ignore.+ignore" }; - settings.handleException = true; - sut.HandleLog("ignore", string.Empty, LogType.Exception); + _settings.ignoreMessages = new[] { "ignore.+ignore" }; + _settings.handleException = true; + _sut.HandleLog("ignore_xxx_ignore", string.Empty, LogType.Exception); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Not.Empty); + Assert.That(_spyTerminatable.IsCalled, Is.False); } [Test] - public async Task HandleLog_InvalidIgnoreMessagePattern_ThrowArgumentExceptionAndReported() + public async Task HandleLog_NotMatchIgnoreMessagePattern_TerminateIsCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); - - settings.ignoreMessages = new[] { "[a" }; // invalid pattern - settings.handleException = true; - - sut.HandleLog("ignore", string.Empty, LogType.Exception); + _settings.ignoreMessages = new[] { "ignore.+ignore" }; + _settings.handleException = true; + _sut.HandleLog("ignore", string.Empty, LogType.Exception); await UniTask.NextFrame(); - LogAssert.Expect(LogType.Exception, "ArgumentException: parsing \"[a\" - Unterminated [] set."); - Assert.That(spyReporter.Arguments, Is.Not.Empty); // Report is executed + Assert.That(_spyTerminatable.IsCalled, Is.True); } [Test] - public async Task HandleLog_StacktraceByLogMessageHandler_notReported() + public async Task HandleLog_InvalidIgnoreMessagePattern_ThrowArgumentExceptionAndTerminateIsCalled() { - var settings = CreateEmptyAutopilotSettings(); - var spyReporter = ScriptableObject.CreateInstance(); - var sut = new LogMessageHandler(settings, spyReporter); + _settings.ignoreMessages = new[] { "[a" }; // invalid pattern + _settings.handleException = true; - settings.handleException = true; - sut.HandleLog(string.Empty, "at DeNA.Anjin.Utilities.LogMessageHandler", LogType.Exception); + _sut.HandleLog("ignore", string.Empty, LogType.Exception); await UniTask.NextFrame(); - Assert.That(spyReporter.Arguments, Is.Empty); + LogAssert.Expect(LogType.Exception, "ArgumentException: parsing \"[a\" - Unterminated [] set."); + Assert.That(_spyTerminatable.IsCalled, Is.True); } - private static AutopilotSettings CreateEmptyAutopilotSettings() + [Test] + public async Task HandleLog_StacktraceByLogMessageHandler_TerminateIsNotCalled() { - var settings = ScriptableObject.CreateInstance(); - settings.ignoreMessages = new string[] { }; - return settings; + _settings.handleException = true; + _sut.HandleLog(string.Empty, "at DeNA.Anjin.Utilities.LogMessageHandler", LogType.Exception); + await UniTask.NextFrame(); + + Assert.That(_spyTerminatable.IsCalled, Is.False); } } } diff --git a/Tests/TestScenes/Buttons.unity b/Tests/TestScenes/Buttons.unity index 0a9a1f5..8dc23d2 100644 --- a/Tests/TestScenes/Buttons.unity +++ b/Tests/TestScenes/Buttons.unity @@ -38,12 +38,12 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 12 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 m_GISettings: serializedVersion: 2 m_BounceScale: 1 @@ -223,7 +223,7 @@ AudioListener: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 277263158} - m_Enabled: 1 + m_Enabled: 0 --- !u!20 &277263160 Camera: m_ObjectHideFlags: 0 @@ -1939,8 +1939,12 @@ Light: m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 m_ShadowRadius: 0 m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 --- !u!4 &1741233951 Transform: m_ObjectHideFlags: 0 diff --git a/Tests/TestScenes/Error.unity b/Tests/TestScenes/Error.unity index bffe5cd..b39c3eb 100644 --- a/Tests/TestScenes/Error.unity +++ b/Tests/TestScenes/Error.unity @@ -13,7 +13,7 @@ OcclusionCullingSettings: --- !u!104 &2 RenderSettings: m_ObjectHideFlags: 0 - serializedVersion: 9 + serializedVersion: 10 m_Fog: 0 m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_FogMode: 3 @@ -38,13 +38,12 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0.44657844, g: 0.49641222, b: 0.57481676, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 12 - m_GIWorkflowMode: 1 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 m_GISettings: serializedVersion: 2 m_BounceScale: 1 @@ -67,9 +66,6 @@ LightmapSettings: m_LightmapParameters: {fileID: 0} m_LightmapsBakeMode: 1 m_TextureCompression: 1 - m_FinalGather: 0 - m_FinalGatherFiltering: 1 - m_FinalGatherRayCount: 256 m_ReflectionCompression: 2 m_MixedBakeMode: 2 m_BakeBackend: 1 @@ -193,6 +189,9 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -335,7 +334,7 @@ AudioListener: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1025330164} - m_Enabled: 1 + m_Enabled: 0 --- !u!20 &1025330166 Camera: m_ObjectHideFlags: 0 @@ -476,6 +475,9 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -561,9 +563,8 @@ Light: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1325883444} m_Enabled: 1 - serializedVersion: 10 + serializedVersion: 11 m_Type: 1 - m_Shape: 0 m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} m_Intensity: 1 m_Range: 10 @@ -613,8 +614,12 @@ Light: m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 m_ShadowRadius: 0 m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 --- !u!4 &1325883446 Transform: m_ObjectHideFlags: 0 diff --git a/Tests/TestScenes/OutGameTutorial.unity b/Tests/TestScenes/OutGameTutorial.unity index 9429ffe..4e70e59 100644 --- a/Tests/TestScenes/OutGameTutorial.unity +++ b/Tests/TestScenes/OutGameTutorial.unity @@ -38,12 +38,12 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0.46169513, g: 0.5124164, b: 0.58993304, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 12 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 m_GISettings: serializedVersion: 2 m_BounceScale: 1 @@ -195,8 +195,12 @@ Light: m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 m_ShadowRadius: 0 m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 --- !u!4 &490633088 Transform: m_ObjectHideFlags: 0 @@ -982,7 +986,7 @@ AudioListener: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1510846388} - m_Enabled: 1 + m_Enabled: 0 --- !u!20 &1510846390 Camera: m_ObjectHideFlags: 0