diff --git a/Editor/Localization/ja.po b/Editor/Localization/ja.po index 75233f7..6daa19a 100644 --- a/Editor/Localization/ja.po +++ b/Editor/Localization/ja.po @@ -87,6 +87,18 @@ msgstr "JUnitレポート出力パス" msgid "JUnit report output path" msgstr "JUnit形式のレポートファイル出力パス(省略時は出力されない)" +# reporter +msgid "Reporter" +msgstr "レポータ" + +# reporter tooltip +msgid "Reporter that called when some errors occurred in target application" +msgstr "対象のアプリケーションで発生したエラーを通知するレポータ" + +# obsolete slack settings +msgid "Slack settings will be moved to SlackReporter" +msgstr "Slack 設定は SlackReporter へ移動しました" + # slackToken msgid "Slack Token" msgstr "Slackトークン" @@ -108,11 +120,11 @@ msgid "Slack Mention Settings" msgstr "Slackメンション設定" # mentionSubTeamIDs -msgid "Mention Sub Team IDs" +msgid "Sub Team IDs to Mention" msgstr "メンション宛先" # mentionSubTeamIDs tooltip -msgid "Mention to sub team ID (comma separates)" +msgid "Sub team IDs to mention (comma separates)" msgstr "Slack通知メッセージでメンションするチームのIDをカンマ区切りで指定します" # addHereInSlackMessage @@ -120,7 +132,7 @@ msgid "Add @here Into Slack Message" msgstr "@hereをメッセージにつける" # addHereInSlackMessage tooltip -msgid "Add @here into Slack message" +msgid "Whether adding @here into Slack messages or not" msgstr "Slack通知メッセージに@hereを付けます" # Header: Error Handling Settings @@ -320,3 +332,7 @@ msgstr "操作を記録したJSONファイル" # recordedJson tooltip msgid "JSON file recorded by AutomatedQA package" msgstr "Automated QAパッケージのRecorded Playbackウィンドウで記録したjsonファイルを設定します" + +# composite reporters +msgid "Reporters" +msgstr "レポータ" diff --git a/Editor/UI/Reporters.meta b/Editor/UI/Reporters.meta new file mode 100644 index 0000000..82538c7 --- /dev/null +++ b/Editor/UI/Reporters.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3c549ea2f5314b23a625a59b05a24220 +timeCreated: 1699484044 \ No newline at end of file diff --git a/Editor/UI/Reporters/CompositeReporterEditor.cs b/Editor/UI/Reporters/CompositeReporterEditor.cs new file mode 100644 index 0000000..0fe7f5f --- /dev/null +++ b/Editor/UI/Reporters/CompositeReporterEditor.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2023 DeNA Co., Ltd. +// This software is released under the MIT License. + +using DeNA.Anjin.Reporters; +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; + +namespace DeNA.Anjin.Editor.UI.Reporters +{ + /// + /// Editor for + /// + [CustomEditor(typeof(CompositeReporter))] + public class CompositeReporterEditor : UnityEditor.Editor + { + private SerializedProperty _reportersProp; + private ReorderableList _reorderableList; + private static readonly string s_reporters = L10n.Tr("Reporters"); + private GUIContent _reportersGUIContent; + private static readonly string s_reporter = L10n.Tr("Reporter"); + private GUIContent _reporterGUIContent; + + + private void OnEnable() + { + Initialize(); + } + + + private void Initialize() + { + _reportersProp = serializedObject.FindProperty(nameof(CompositeReporter.reporters)); + _reportersGUIContent = new GUIContent(s_reporters); + _reporterGUIContent = new GUIContent(s_reporter); + _reorderableList = new ReorderableList(serializedObject, _reportersProp) + { + drawHeaderCallback = rect => EditorGUI.LabelField(rect, _reportersGUIContent), + elementHeightCallback = _ => EditorGUIUtility.singleLineHeight, + // XXX: Dont use discarded parameter to treat Unity 2019.x + // ReSharper disable UnusedParameter.Local + drawElementCallback = (rect, index, active, focused) => + // ReSharper restore UnusedParameter.Local + { + var elemProp = _reportersProp.GetArrayElementAtIndex(index); + EditorGUI.PropertyField(rect, elemProp, _reporterGUIContent); + } + }; + } + + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + _reorderableList.DoLayoutList(); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/UI/Reporters/CompositeReporterEditor.cs.meta b/Editor/UI/Reporters/CompositeReporterEditor.cs.meta new file mode 100644 index 0000000..e6783ba --- /dev/null +++ b/Editor/UI/Reporters/CompositeReporterEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ccf304a0e83345eba515322942248039 +timeCreated: 1699484060 \ No newline at end of file diff --git a/Editor/UI/Reporters/SlackReporterEditor.cs b/Editor/UI/Reporters/SlackReporterEditor.cs new file mode 100644 index 0000000..292b382 --- /dev/null +++ b/Editor/UI/Reporters/SlackReporterEditor.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2023 DeNA Co., Ltd. +// This software is released under the MIT License. + +using DeNA.Anjin.Reporters; +using UnityEditor; +using UnityEngine; + +namespace DeNA.Anjin.Editor.UI.Reporters +{ + /// + /// Editor for + /// + [CustomEditor(typeof(SlackReporter))] + public class SlackReporterEditor : UnityEditor.Editor + { + private const float SpacerPixels = 10f; + private const float SpacerPixelsUnderHeader = 4f; + + private static readonly string s_slackToken = L10n.Tr("Slack Token"); + private static readonly string s_slackTokenTooltip = L10n.Tr("Slack API token"); + private SerializedProperty _slackTokenProp; + private GUIContent _slackTokenGUIContent; + + private static readonly string s_slackChannels = L10n.Tr("Slack Channels"); + private static readonly string s_slackChannelsTooltip = L10n.Tr("Slack channels to send notification"); + 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; + private GUIContent _mentionSubTeamIDsGUIContent; + + private static readonly string s_addHereInSlackMessage = L10n.Tr("Add @here Into Slack Message"); + private static readonly string s_addHereInSlackMessageTooltip = L10n.Tr("Whether adding @here into Slack messages or not"); + private SerializedProperty _addHereInSlackMessageProp; + private GUIContent _addHereInSlackMessageGUIContent; + + + private void OnEnable() + { + Initialize(); + } + + + private void Initialize() + { + _slackTokenProp = serializedObject.FindProperty(nameof(SlackReporter.slackToken)); + _slackTokenGUIContent = new GUIContent(s_slackToken, s_slackTokenTooltip); + + _slackChannelsProp = serializedObject.FindProperty(nameof(SlackReporter.slackChannels)); + _slackChannelsGUIContent = new GUIContent(s_slackChannels, s_slackChannelsTooltip); + + _mentionSubTeamIDsProp = serializedObject.FindProperty(nameof(SlackReporter.mentionSubTeamIDs)); + _mentionSubTeamIDsGUIContent = new GUIContent(s_mentionSubTeamIDs, s_mentionSubTeamIDsTooltip); + + _addHereInSlackMessageProp = serializedObject.FindProperty(nameof(SlackReporter.addHereInSlackMessage)); + _addHereInSlackMessageGUIContent = new GUIContent(s_addHereInSlackMessage, s_addHereInSlackMessageTooltip); + } + + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField(_slackTokenProp, _slackTokenGUIContent); + EditorGUILayout.PropertyField(_slackChannelsProp, _slackChannelsGUIContent); + + GUILayout.Space(SpacerPixels); + GUILayout.Label(s_slackMentionSettingsHeader); + GUILayout.Space(SpacerPixelsUnderHeader); + + EditorGUILayout.PropertyField(_mentionSubTeamIDsProp, _mentionSubTeamIDsGUIContent); + EditorGUILayout.PropertyField(_addHereInSlackMessageProp, _addHereInSlackMessageGUIContent); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/UI/Reporters/SlackReporterEditor.cs.meta b/Editor/UI/Reporters/SlackReporterEditor.cs.meta new file mode 100644 index 0000000..d061868 --- /dev/null +++ b/Editor/UI/Reporters/SlackReporterEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 99b990767b4d4abe95b829b1d62b7698 +timeCreated: 1699484320 \ No newline at end of file diff --git a/Editor/UI/Settings/AutopilotSettingsEditor.cs b/Editor/UI/Settings/AutopilotSettingsEditor.cs index 86a127a..abd5f44 100644 --- a/Editor/UI/Settings/AutopilotSettingsEditor.cs +++ b/Editor/UI/Settings/AutopilotSettingsEditor.cs @@ -42,16 +42,19 @@ public class AutopilotSettingsEditor : UnityEditor.Editor private static readonly string s_junitReportPath = L10n.Tr("JUnit Report Path"); private static readonly string s_junitReportPathTooltip = L10n.Tr("JUnit report output path"); + private static readonly string s_reporter = L10n.Tr("Reporter"); + private static readonly string s_reporterTooltip = L10n.Tr("Reporter that called when some errors occurred in target application"); + private static readonly string s_slackToken = L10n.Tr("Slack Token"); private static readonly string s_slackTokenTooltip = L10n.Tr("Slack API token"); private static readonly string s_slackChannels = L10n.Tr("Slack Channels"); private static readonly string s_slackChannelsTooltip = L10n.Tr("Slack channels to send notification"); private static readonly string s_slackMentionSettingsHeader = L10n.Tr("Slack Mention Settings"); - private static readonly string s_mentionSubTeamIDs = L10n.Tr("Mention Sub Team IDs"); - private static readonly string s_mentionSubTeamIDsTooltip = L10n.Tr("Mention to sub team ID (comma separates)"); + 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 static readonly string s_addHereInSlackMessage = L10n.Tr("Add @here Into Slack Message"); - private static readonly string s_addHereInSlackMessageTooltip = L10n.Tr("Add @here into Slack message"); + private static readonly string s_addHereInSlackMessageTooltip = L10n.Tr("Whether adding @here into Slack messages or not"); private static readonly string s_errorHandlingSettingsHeader = L10n.Tr("Error Handling Settings"); private static readonly string s_handleException = L10n.Tr("Handle Exception"); @@ -64,6 +67,9 @@ public class AutopilotSettingsEditor : UnityEditor.Editor 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_obsoletedSlackParamsHelpBox = + L10n.Tr("Slack settings will be moved to SlackReporter"); + private static readonly string s_ignoreMessagesTooltip = L10n.Tr("Do not send notifications when log messages contain this string"); @@ -105,6 +111,9 @@ public override void OnInspectorGUI() new GUIContent(s_timeScale, s_timeScaleTooltip)); EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.junitReportPath)), new GUIContent(s_junitReportPath, s_junitReportPathTooltip)); + EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.reporter)), + new GUIContent(s_reporter, s_reporterTooltip)); + EditorGUILayout.HelpBox(s_obsoletedSlackParamsHelpBox, MessageType.Warning); EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.slackToken)), new GUIContent(s_slackToken, s_slackTokenTooltip)); EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(AutopilotSettings.slackChannels)), diff --git a/Runtime/Autopilot.cs b/Runtime/Autopilot.cs index 84a99c8..f6d931e 100644 --- a/Runtime/Autopilot.cs +++ b/Runtime/Autopilot.cs @@ -43,7 +43,6 @@ public enum ExitCode private ILogger _logger; private RandomFactory _randomFactory; private IAgentDispatcher _dispatcher; - private IReporter _reporter; private LogMessageHandler _logMessageHandler; private AutopilotState _state; private AutopilotSettings _settings; @@ -55,7 +54,7 @@ private void Start() _settings = _state.settings; Assert.IsNotNull(_settings); - _logger = new ConsoleLogger(Debug.unityLogger.logHandler); + _logger = CreateLogger(); if (!int.TryParse(_settings.randomSeed, out var seed)) { @@ -68,8 +67,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. - _reporter = new SlackReporter(_settings, new SlackAPI()); - _logMessageHandler = new LogMessageHandler(_settings, _reporter); + _logMessageHandler = new LogMessageHandler(_settings, _settings.reporter); Application.logMessageReceivedThreaded += _logMessageHandler.HandleLog; _dispatcher = new AgentDispatcher(_settings, _logger, _randomFactory); @@ -89,6 +87,15 @@ private void Start() _startTime = Time.realtimeSinceStartup; } + /// + /// Returns an agent dispatcher that autopilot uses. You can change a logger by overriding this method + /// + /// A new logger + protected virtual ILogger CreateLogger() + { + return new ConsoleLogger(Debug.unityLogger.logHandler); + } + /// /// Terminate when ran specified time. /// diff --git a/Runtime/Reporters/IReporter.cs b/Runtime/Reporters/AbstractReporter.cs similarity index 52% rename from Runtime/Reporters/IReporter.cs rename to Runtime/Reporters/AbstractReporter.cs index 7f85a8a..c40db21 100644 --- a/Runtime/Reporters/IReporter.cs +++ b/Runtime/Reporters/AbstractReporter.cs @@ -1,24 +1,35 @@ // Copyright (c) 2023 DeNA Co., Ltd. // This software is released under the MIT License. +using System.Threading; using Cysharp.Threading.Tasks; +using DeNA.Anjin.Settings; using UnityEngine; namespace DeNA.Anjin.Reporters { /// - /// Reporter interface + /// Reporter base class /// - public interface IReporter + public abstract class AbstractReporter : ScriptableObject { /// /// Post report log message, stacktrace and screenshot /// + /// Autopilot settings /// Log message /// Stack trace /// Log message type /// With screenshot + /// Cancellation token /// - UniTask PostReportAsync(string logString, string stackTrace, LogType type, bool withScreenshot); + public abstract UniTask PostReportAsync( + AutopilotSettings settings, + string logString, + string stackTrace, + LogType type, + bool withScreenshot, + CancellationToken cancellationToken = default + ); } } diff --git a/Runtime/Reporters/IReporter.cs.meta b/Runtime/Reporters/AbstractReporter.cs.meta similarity index 100% rename from Runtime/Reporters/IReporter.cs.meta rename to Runtime/Reporters/AbstractReporter.cs.meta diff --git a/Runtime/Reporters/CompositeReporter.cs b/Runtime/Reporters/CompositeReporter.cs new file mode 100644 index 0000000..e9fea76 --- /dev/null +++ b/Runtime/Reporters/CompositeReporter.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2023 DeNA Co., Ltd. +// This software is released under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Cysharp.Threading.Tasks; +using DeNA.Anjin.Settings; +using UnityEngine; + +namespace DeNA.Anjin.Reporters +{ + /// + /// A class for reporters that delegate to multiple reporters + /// + [CreateAssetMenu(fileName = "New CompositeReporter", menuName = "Anjin/Composite Reporter", order = 51)] + public class CompositeReporter : AbstractReporter + { + /// + /// Reporters to delegate + /// + public List reporters = new List(); + + + /// + public override async UniTask PostReportAsync( + AutopilotSettings settings, + string logString, + string stackTrace, + LogType type, + bool withScreenshot, + CancellationToken cancellationToken = default + ) + { + await UniTask.WhenAll( + reporters + .Where(r => r != this && r != null) + .Select( + r => r.PostReportAsync(settings, logString, stackTrace, type, withScreenshot, cancellationToken) + ) + ); + } + } +} diff --git a/Runtime/Reporters/CompositeReporter.cs.meta b/Runtime/Reporters/CompositeReporter.cs.meta new file mode 100644 index 0000000..7aeb2c1 --- /dev/null +++ b/Runtime/Reporters/CompositeReporter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 107581b5731b4e6d824ae77242f55dd3 +timeCreated: 1699480101 \ No newline at end of file diff --git a/Runtime/Reporters/SlackMessageSender.cs b/Runtime/Reporters/SlackMessageSender.cs new file mode 100644 index 0000000..c97eb7f --- /dev/null +++ b/Runtime/Reporters/SlackMessageSender.cs @@ -0,0 +1,154 @@ +// Copyright (c) 2023 DeNA Co., Ltd. +// This software is released under the MIT License. + +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace DeNA.Anjin.Reporters +{ + /// + /// An interface for Slack message senders. The derived class of this interface must define message format, and + /// delegate touching Slack API to . + /// Purpose of introducing this interface is making be a humble object. + /// + public interface ISlackMessageSender + { + /// + /// Post report log message, stacktrace and screenshot + /// + /// Slack API token + /// Slack Channel to send notification + /// Sub team IDs to mention + /// Whether adding @here or not + /// Log message + /// Stack trace + /// With screenshot + /// Cancellation token + /// + UniTask Send( + string slackToken, + string slackChannel, + IEnumerable mentionSubTeamIDs, + bool addHereInSlackMessage, + string logString, + string stackTrace, + bool withScreenshot, + CancellationToken cancellationToken = default + ); + } + + /// + /// A class for Slack message senders. This class defines a message format and delegates touching Slack API to + /// . + /// + public class SlackMessageSender : ISlackMessageSender + { + private readonly SlackAPI _slackAPI; + private readonly string _slackToken; + + + /// + /// Creates a new instance for . + /// + /// Slack API client + public SlackMessageSender(SlackAPI api) + { + _slackAPI = api; + } + + + /// + public async UniTask Send( + string slackToken, + string slackChannel, + IEnumerable mentionSubTeamIDs, + bool addHereInSlackMessage, + string logString, + string stackTrace, + bool withScreenshot, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrEmpty(slackToken) || string.IsNullOrEmpty(slackChannel)) + { + return; + } + + var title = Title(logString, mentionSubTeamIDs, addHereInSlackMessage); + + await UniTask.SwitchToMainThread(); + + var postTitleTask = await _slackAPI.Post( + slackToken, + slackChannel, + title, + cancellationToken: cancellationToken + ); + if (!postTitleTask.Success) + { + return; + } + + if (withScreenshot && !Application.isBatchMode) + { + var coroutineRunner = new GameObject().AddComponent(); + await UniTask.WaitForEndOfFrame(coroutineRunner); + Object.Destroy(coroutineRunner); + + var screenshot = ScreenCapture.CaptureScreenshotAsTexture(); + var withoutAlpha = new Texture2D(screenshot.width, screenshot.height, TextureFormat.RGB24, false); + withoutAlpha.SetPixels(screenshot.GetPixels()); + withoutAlpha.Apply(); + + var postScreenshotTask = await _slackAPI.Post( + slackToken, + slackChannel, + withoutAlpha.EncodeToPNG(), + postTitleTask.Ts + ); + if (!postScreenshotTask.Success) + { + return; + } + } + + var body = Body(logString, stackTrace); + await _slackAPI.Post(slackToken, slackChannel, body, postTitleTask.Ts); + } + + + private static string Title(string logString, IEnumerable mentionSubTeamIDs, bool withHere) + { + var sb = new StringBuilder(); + foreach (var s in mentionSubTeamIDs) + { + if (!string.IsNullOrEmpty(s)) + { + // ReSharper disable once StringLiteralTypo + sb.Append($" "); + } + } + + if (withHere) + { + sb.Append(" "); + } + + sb.Append(logString); + return sb.ToString(); + } + + + private static string Body(string logString, string stackTrace) + { + return $"{logString}\n\n```{stackTrace}```"; + } + + private class CoroutineRunner : MonoBehaviour + { + } + } +} diff --git a/Runtime/Reporters/SlackMessageSender.cs.meta b/Runtime/Reporters/SlackMessageSender.cs.meta new file mode 100644 index 0000000..87e16a2 --- /dev/null +++ b/Runtime/Reporters/SlackMessageSender.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4bd66e2697dd4869b5a7fc506dc5a027 +timeCreated: 1699480963 \ No newline at end of file diff --git a/Runtime/Reporters/SlackReporter.cs b/Runtime/Reporters/SlackReporter.cs index b7014ba..0574388 100644 --- a/Runtime/Reporters/SlackReporter.cs +++ b/Runtime/Reporters/SlackReporter.cs @@ -1,7 +1,7 @@ // Copyright (c) 2023 DeNA Co., Ltd. // This software is released under the MIT License. -using System.Text; +using System.Threading; using Cysharp.Threading.Tasks; using DeNA.Anjin.Settings; using UnityEngine; @@ -11,111 +11,62 @@ namespace DeNA.Anjin.Reporters /// /// Post report to Slack /// - public class SlackReporter : IReporter + [CreateAssetMenu(fileName = "New SlackReporter", menuName = "Anjin/Slack Reporter", order = 50)] + public class SlackReporter : AbstractReporter { - private readonly AutopilotSettings _settings; - private readonly SlackAPI _slackAPI; - /// - /// Constructor + /// Slack API token /// - /// - /// - public SlackReporter(AutopilotSettings settings, SlackAPI slackAPI) - { - _settings = settings; - _slackAPI = slackAPI; - } + public string slackToken; /// - /// Post report log message, stacktrace and screenshot to Slack + /// Slack channels to send notification (comma separated) /// - /// Log message string - /// Stack trace - /// Log message type - /// With screenshot - /// - public async UniTask PostReportAsync(string logString, string stackTrace, LogType type, bool withScreenshot) - { - if (string.IsNullOrEmpty(_settings.slackToken) || string.IsNullOrEmpty(_settings.slackChannels)) - { - return; - } - - var title = Title(logString, _settings.mentionSubTeamIDs, _settings.addHereInSlackMessage); - var body = Body(logString, stackTrace); - - await UniTask.SwitchToMainThread(); - - foreach (var channel in _settings.slackChannels.Split(',')) - { - if (string.IsNullOrEmpty(channel)) - { - continue; - } - - var postTitleTask = await _slackAPI.Post(_settings.slackToken, channel, title); - if (!postTitleTask.Success) - { - return; - } - - if (withScreenshot && !Application.isBatchMode) - { - var coroutineRunner = new GameObject().AddComponent(); - await UniTask.WaitForEndOfFrame(coroutineRunner); - Object.Destroy(coroutineRunner); - - var screenshot = ScreenCapture.CaptureScreenshotAsTexture(); - var withoutAlpha = new Texture2D(screenshot.width, screenshot.height, TextureFormat.RGB24, false); - withoutAlpha.SetPixels(screenshot.GetPixels()); - withoutAlpha.Apply(); - - var postScreenshotTask = await _slackAPI.Post(_settings.slackToken, channel, - withoutAlpha.EncodeToPNG(), postTitleTask.Ts); - if (!postScreenshotTask.Success) - { - return; - } - } + public string slackChannels; - var postBodyTask = await _slackAPI.Post(_settings.slackToken, channel, body, - postTitleTask.Ts); - if (!postBodyTask.Success) - { - return; - } - } - } + /// + /// Sub team IDs to mention (comma separated) + /// + public string mentionSubTeamIDs; - private static string Title(string logString, string mentionSubTeamIDs, bool withHere) + /// + /// Whether adding @here or not + /// + public bool addHereInSlackMessage; + + private readonly ISlackMessageSender _sender = new SlackMessageSender(new SlackAPI()); + + /// + public override async UniTask PostReportAsync( + AutopilotSettings settings, + string logString, + string stackTrace, + LogType type, + bool withScreenshot, + CancellationToken cancellationToken = default + ) { - var title = new StringBuilder(); - - foreach (var s in mentionSubTeamIDs.Split(',')) + // NOTE: In _sender.send, switch the execution thread to the main thread, so UniTask.WhenAll is meaningless. + foreach (var slackChannel in (string.IsNullOrEmpty(settings.slackChannels) + ? slackChannels + : settings.slackChannels).Split(",")) { - if (!string.IsNullOrEmpty(s)) + if (cancellationToken.IsCancellationRequested) { - // ReSharper disable once StringLiteralTypo - title.Append($" "); + return; } + + await _sender.Send( + string.IsNullOrEmpty(settings.slackToken) ? slackToken : settings.slackToken, + slackChannel, + mentionSubTeamIDs.Split(","), + addHereInSlackMessage, + logString, + stackTrace, + withScreenshot, + cancellationToken + ); } - - if (withHere) - { - title.Append(" "); - } - - return title.Append(logString).ToString(); - } - - private static string Body(string logString, string stackTrace) - { - return $"{logString}\n\n```{stackTrace}```"; - } - - private class CoroutineRunner : MonoBehaviour - { } } } diff --git a/Runtime/Settings/AutopilotSettings.cs b/Runtime/Settings/AutopilotSettings.cs index 1a4a79b..78279a0 100644 --- a/Runtime/Settings/AutopilotSettings.cs +++ b/Runtime/Settings/AutopilotSettings.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using DeNA.Anjin.Agents; +using DeNA.Anjin.Reporters; using UnityEngine; namespace DeNA.Anjin.Settings @@ -78,21 +79,25 @@ public class AutopilotSettings : ScriptableObject /// /// Slack API token /// + [Obsolete("this option moved to SlackReporter")] public string slackToken; /// /// Slack channels to send notification /// + [Obsolete("this option moved to SlackReporter")] public string slackChannels; /// /// Mention to sub team ID (comma separates) /// + [Obsolete("this option moved to SlackReporter")] public string mentionSubTeamIDs; /// /// Add @here /// + [Obsolete("this option moved to SlackReporter")] public bool addHereInSlackMessage; /// @@ -120,6 +125,11 @@ public class AutopilotSettings : ScriptableObject /// public string[] ignoreMessages; + /// + /// Reporter that called when some errors occurred + /// + public AbstractReporter reporter; + /// /// Overwrites specified values in the command line arguments /// @@ -147,12 +157,16 @@ public void OverrideByCommandLineArguments(Arguments args) if (args.SlackToken.IsCaptured()) { +#pragma warning disable CS0618 // Type or member is obsolete slackToken = args.SlackToken.Value(); +#pragma warning restore CS0618 // Type or member is obsolete } if (args.SlackChannels.IsCaptured()) { +#pragma warning disable CS0618 // Type or member is obsolete slackChannels = args.SlackChannels.Value(); +#pragma warning restore CS0618 // Type or member is obsolete } } } diff --git a/Runtime/Utilities/LogMessageHandler.cs b/Runtime/Utilities/LogMessageHandler.cs index 1a369e6..6563470 100644 --- a/Runtime/Utilities/LogMessageHandler.cs +++ b/Runtime/Utilities/LogMessageHandler.cs @@ -14,14 +14,14 @@ namespace DeNA.Anjin.Utilities public class LogMessageHandler { private readonly AutopilotSettings _settings; - private readonly IReporter _reporter; + private readonly AbstractReporter _reporter; /// /// Constructor /// /// Autopilot settings /// Reporter implementation - public LogMessageHandler(AutopilotSettings settings, IReporter reporter) + public LogMessageHandler(AutopilotSettings settings, AbstractReporter reporter) { _settings = settings; _reporter = reporter; @@ -42,7 +42,11 @@ public async void HandleLog(string logString, string stackTrace, LogType type) // NOTE: HandleLog may called by non-main thread because it subscribe Application.logMessageReceivedThreaded await UniTask.SwitchToMainThread(); - await _reporter.PostReportAsync(logString, stackTrace, type, true); + + if (_reporter != null) + { + await _reporter.PostReportAsync(_settings, logString, stackTrace, type, true); + } var autopilot = Object.FindObjectOfType(); if (autopilot != null) diff --git a/Tests/Editor/DeNA.Anjin.Editor.Tests.asmdef b/Tests/Editor/DeNA.Anjin.Editor.Tests.asmdef index 6c7f934..c4492e6 100644 --- a/Tests/Editor/DeNA.Anjin.Editor.Tests.asmdef +++ b/Tests/Editor/DeNA.Anjin.Editor.Tests.asmdef @@ -7,7 +7,8 @@ "DeNA.Anjin", "DeNA.Anjin.Annotations", "DeNA.Anjin.Editor", - "UniTask" + "UniTask", + "DeNA.Anjin.Tests" ], "includePlatforms": [ "Editor" diff --git a/Tests/Runtime/Reporters.meta b/Tests/Runtime/Reporters.meta new file mode 100644 index 0000000..3b23278 --- /dev/null +++ b/Tests/Runtime/Reporters.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 84a88262c36d4c7e8b5e92add0851edf +timeCreated: 1699486146 \ No newline at end of file diff --git a/Tests/Runtime/Reporters/SlackMessageSenderTest.cs b/Tests/Runtime/Reporters/SlackMessageSenderTest.cs new file mode 100644 index 0000000..359ac18 --- /dev/null +++ b/Tests/Runtime/Reporters/SlackMessageSenderTest.cs @@ -0,0 +1,182 @@ +// Copyright (c) 2023 DeNA Co., Ltd. +// This software is released under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DeNA.Anjin.TestDoubles; +using NUnit.Framework; + +namespace DeNA.Anjin.Reporters +{ + [TestFixture] + public class SlackMessageSenderTest + { + [Test] + public async Task NoMentionsAndNoScreenshotsAndNoAtHere() + { + var spySlackAPI = new SpySlackAPI(); + var sender = new SlackMessageSender(spySlackAPI); + + await sender.Send( + "TOKEN", + "CHANNEL", + Array.Empty(), + false, + "MESSAGE", + "STACKTRACE", + false + ); + + var actual = spySlackAPI.Arguments; + var expected = new List> + { + new Dictionary + { + { "token", "TOKEN" }, { "channel", "CHANNEL" }, { "message", "MESSAGE" }, { "ts", null } + }, + new Dictionary + { + { "token", "TOKEN" }, + { "channel", "CHANNEL" }, + { "message", "MESSAGE\n\n```STACKTRACE```" }, + { "ts", "1" } + }, + }; + Assert.That(actual, Is.EqualTo(expected), Format(actual)); + } + + [Test] + public async Task WithMention() + { + var spySlackAPI = new SpySlackAPI(); + var sender = new SlackMessageSender(spySlackAPI); + + await sender.Send( + "TOKEN", + "CHANNEL", + new []{"MENTION1", "MENTION2"}, + false, + "MESSAGE", + "STACKTRACE", + false + ); + + var actual = spySlackAPI.Arguments; + var expected = new List> + { + new Dictionary + { + { "token", "TOKEN" }, { "channel", "CHANNEL" }, { "message", " MESSAGE" }, { "ts", null } + }, + new Dictionary + { + { "token", "TOKEN" }, + { "channel", "CHANNEL" }, + { "message", "MESSAGE\n\n```STACKTRACE```" }, + { "ts", "1" } + }, + }; + Assert.That(actual, Is.EqualTo(expected), Format(actual)); + } + + [Test] + public async Task WithScreenshot() + { + var spySlackAPI = new SpySlackAPI(); + var sender = new SlackMessageSender(spySlackAPI); + + await sender.Send( + "TOKEN", + "CHANNEL", + new []{"MENTION1", "MENTION2"}, + false, + "MESSAGE", + "STACKTRACE", + true + ); + + var actual = spySlackAPI.Arguments; + var expected = new List> + { + new Dictionary + { + { "token", "TOKEN" }, { "channel", "CHANNEL" }, { "message", " MESSAGE" }, { "ts", null } + }, + new Dictionary + { + { "token", "TOKEN" }, { "channel", "CHANNEL" }, { "message", "IMAGE" }, { "ts", "1" } + }, + new Dictionary + { + { "token", "TOKEN" }, + { "channel", "CHANNEL" }, + { "message", "MESSAGE\n\n```STACKTRACE```" }, + { "ts", "1" } + }, + }; + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public async Task WithAtHere() + { + var spySlackAPI = new SpySlackAPI(); + var sender = new SlackMessageSender(spySlackAPI); + + await sender.Send( + "TOKEN", + "CHANNEL", + Array.Empty(), + true, + "MESSAGE", + "STACKTRACE", + false + ); + + var actual = spySlackAPI.Arguments; + var expected = new List> + { + new Dictionary + { + { "token", "TOKEN" }, { "channel", "CHANNEL" }, { "message", " MESSAGE" }, { "ts", null } + }, + new Dictionary + { + { "token", "TOKEN" }, + { "channel", "CHANNEL" }, + { "message", "MESSAGE\n\n```STACKTRACE```" }, + { "ts", "1" } + }, + }; + Assert.That(actual, Is.EqualTo(expected), Format(actual)); + } + + + private static string Format(List> dicts) + { + var sb = new StringBuilder(); + sb.AppendLine("["); + foreach (var dict in dicts) + { + sb.AppendLine("\t{"); + var keys = dict.Keys.ToArray(); + Array.Sort(keys); + foreach (var key in keys) + { + var value = dict[key]; + sb.Append("\t\t"); + sb.Append(key); + sb.Append(": "); + sb.Append(value); + sb.AppendLine(","); + } + sb.AppendLine("\t},"); + } + sb.AppendLine("]"); + return sb.ToString(); + } + } +} diff --git a/Tests/Runtime/Reporters/SlackMessageSenderTest.cs.meta b/Tests/Runtime/Reporters/SlackMessageSenderTest.cs.meta new file mode 100644 index 0000000..b8d2786 --- /dev/null +++ b/Tests/Runtime/Reporters/SlackMessageSenderTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 69a1be0e7f9c4e0aa62cb7d0884e5459 +timeCreated: 1699486165 \ No newline at end of file diff --git a/Tests/Runtime/TestDoubles/SpyReporter.cs b/Tests/Runtime/TestDoubles/SpyReporter.cs new file mode 100644 index 0000000..a1aca42 --- /dev/null +++ b/Tests/Runtime/TestDoubles/SpyReporter.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2023 DeNA Co., Ltd. +// This software is released under the MIT License. + +using System.Collections.Generic; +using System.Threading; +using Cysharp.Threading.Tasks; +using DeNA.Anjin.Reporters; +using DeNA.Anjin.Settings; +using UnityEngine; + +namespace DeNA.Anjin.TestDoubles +{ + /// + /// A spy for + /// + // [CreateAssetMenu(fileName = "New SpyReporter", menuName = "Anjin/Spy Reporter", order = 34)] + public class SpyReporter : AbstractReporter + { + public List> Arguments { get; } = new List>(); + + public override async UniTask PostReportAsync( + AutopilotSettings settings, + string logString, + string stackTrace, + LogType type, + bool withScreenshot, + CancellationToken cancellationToken = default + ) + { + Debug.Log("Reporter called"); + Arguments.Add(new Dictionary + { + {"settings", settings.ToString()}, + {"logString", logString}, + {"stackTrace", stackTrace}, + {"type", type.ToString()}, + {"withScreenshot", withScreenshot.ToString()} + }); + await UniTask.NextFrame(cancellationToken); + } + } +} diff --git a/Tests/Runtime/TestDoubles/SpyReporter.cs.meta b/Tests/Runtime/TestDoubles/SpyReporter.cs.meta new file mode 100644 index 0000000..fcd1990 --- /dev/null +++ b/Tests/Runtime/TestDoubles/SpyReporter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fbb88a2914494167b3e9f91d4dc51fdf +timeCreated: 1699485033 \ No newline at end of file diff --git a/Tests/Runtime/TestDoubles/StubThrowingErrorFromNotMainThreadOnClick.cs.meta b/Tests/Runtime/TestDoubles/StubThrowingErrorFromNotMainThreadOnClick.cs.meta index 37b8bb9..b7e3fd4 100644 --- a/Tests/Runtime/TestDoubles/StubThrowingErrorFromNotMainThreadOnClick.cs.meta +++ b/Tests/Runtime/TestDoubles/StubThrowingErrorFromNotMainThreadOnClick.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 guid: c733c26bf53b475ea76992de2fcc418a -timeCreated: 1699513479 \ No newline at end of file +timeCreated: 1699513479 diff --git a/Tests/Runtime/Utilities/LogMessageHandlerTest.cs b/Tests/Runtime/Utilities/LogMessageHandlerTest.cs index d59cfdd..af45411 100644 --- a/Tests/Runtime/Utilities/LogMessageHandlerTest.cs +++ b/Tests/Runtime/Utilities/LogMessageHandlerTest.cs @@ -1,10 +1,8 @@ // Copyright (c) 2023 DeNA Co., Ltd. // This software is released under the MIT License. -using System.Collections.Generic; using System.Threading.Tasks; using Cysharp.Threading.Tasks; -using DeNA.Anjin.Reporters; using DeNA.Anjin.Settings; using DeNA.Anjin.TestDoubles; using NUnit.Framework; @@ -19,339 +17,158 @@ public class LogMessageHandlerTest public async Task HandleLog_LogTypeIsLog_notReported() { var settings = ScriptableObject.CreateInstance(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); sut.HandleLog(string.Empty, string.Empty, LogType.Log); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Empty); + Assert.That(spyReporter.Arguments, Is.Empty); } [Test] public async Task HandleLog_LogTypeExceptionHandle_reported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleException = true; sut.HandleLog(string.Empty, string.Empty, LogType.Exception); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Not.Empty); + Assert.That(spyReporter.Arguments, Is.Not.Empty); } [Test] public async Task HandleLog_LogTypeExceptionNotHandle_notReported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleException = false; sut.HandleLog(string.Empty, string.Empty, LogType.Exception); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Empty); + Assert.That(spyReporter.Arguments, Is.Empty); } [Test] public async Task HandleLog_LogTypeAssertHandle_reported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleAssert = true; sut.HandleLog(string.Empty, string.Empty, LogType.Assert); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Not.Empty); + Assert.That(spyReporter.Arguments, Is.Not.Empty); } [Test] public async Task HandleLog_LogTypeAssertNotHandle_notReported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleAssert = false; sut.HandleLog(string.Empty, string.Empty, LogType.Assert); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Empty); + Assert.That(spyReporter.Arguments, Is.Empty); } [Test] public async Task HandleLog_LogTypeErrorHandle_reported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleError = true; sut.HandleLog(string.Empty, string.Empty, LogType.Error); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Not.Empty); + Assert.That(spyReporter.Arguments, Is.Not.Empty); } [Test] public async Task HandleLog_LogTypeErrorNotHandle_notReported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleError = false; sut.HandleLog(string.Empty, string.Empty, LogType.Error); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Empty); + Assert.That(spyReporter.Arguments, Is.Empty); } [Test] public async Task HandleLog_LogTypeWarningHandle_reported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleWarning = true; sut.HandleLog(string.Empty, string.Empty, LogType.Warning); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Not.Empty); + Assert.That(spyReporter.Arguments, Is.Not.Empty); } [Test] public async Task HandleLog_LogTypeWarningNotHandle_notReported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleWarning = false; sut.HandleLog(string.Empty, string.Empty, LogType.Warning); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Empty); + Assert.That(spyReporter.Arguments, Is.Empty); } [Test] public async Task HandleLog_ContainsIgnoreMessage_notReported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + 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); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Empty); + Assert.That(spyReporter.Arguments, Is.Empty); } [Test] public async Task HandleLog_StacktraceByLogMessageHandler_notReported() { var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); + var spyReporter = ScriptableObject.CreateInstance(); + var sut = new LogMessageHandler(settings, spyReporter); settings.handleException = true; sut.HandleLog(string.Empty, "at DeNA.Anjin.Utilities.LogMessageHandler", LogType.Exception); await UniTask.NextFrame(); - Assert.That(spySlackAPI.Arguments, Is.Empty); - } - - [Test] - public async Task HandleLog_StacktraceBySlackAPI_notReported() - { - var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); - - settings.handleException = true; - sut.HandleLog(string.Empty, "at DeNA.Anjin.Reporters.SlackAPI", LogType.Exception); - await UniTask.NextFrame(); - - Assert.That(spySlackAPI.Arguments, Is.Empty); - } - - [Test] - [Category("IgnoreCI")] // Reason: screen capture is not work run on batchmode - public async Task HandleLog_SingleChannel_PostToSlack() - { - var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); - - settings.slackToken = "TOKEN"; - settings.slackChannels = "CHANNEL"; - settings.mentionSubTeamIDs = string.Empty; - settings.addHereInSlackMessage = false; - settings.handleException = true; - sut.HandleLog("MESSAGE", "STACKTRACE", LogType.Exception); - await UniTask.DelayFrame(5); - - var expected1 = new Dictionary - { - { "token", "TOKEN" }, { "channel", "CHANNEL" }, { "message", "MESSAGE" }, { "ts", null } - }; - var expected2 = new Dictionary - { - { "token", "TOKEN" }, { "channel", "CHANNEL" }, { "message", "IMAGE" }, { "ts", "1" } - }; - var expected3 = new Dictionary - { - { "token", "TOKEN" }, - { "channel", "CHANNEL" }, - { "message", "MESSAGE\n\n```STACKTRACE```" }, - { "ts", "1" } - }; - var expected = new List> { expected1, expected2, expected3 }; - var actual = spySlackAPI.Arguments; - - Assert.That(actual, Is.EqualTo(expected)); - } - - [Test] - public async Task HandleLog_SingleChannelWithMention_PostToSlack() - { - var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); - - settings.slackToken = "TOKEN"; - settings.slackChannels = "CHANNEL"; - settings.mentionSubTeamIDs = "MENTION1,MENTION2"; - settings.addHereInSlackMessage = false; - settings.handleException = true; - sut.HandleLog("MESSAGE", "STACKTRACE", LogType.Exception); - await UniTask.DelayFrame(5); - - var expected1 = new Dictionary - { - { "token", "TOKEN" }, - { "channel", "CHANNEL" }, - { "message", " MESSAGE" }, - { "ts", null } - }; - var actual = spySlackAPI.Arguments; - - Assert.That(actual[0], Is.EqualTo(expected1)); - } - - [Test] - public async Task HandleLog_SingleChannelWithHere_PostToSlack() - { - var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); - - settings.slackToken = "TOKEN"; - settings.slackChannels = "CHANNEL"; - settings.mentionSubTeamIDs = string.Empty; - settings.addHereInSlackMessage = true; // add `@here` - settings.handleException = true; - sut.HandleLog("MESSAGE", "STACKTRACE", LogType.Exception); - await UniTask.DelayFrame(5); - - var expected1 = new Dictionary - { - { "token", "TOKEN" }, { "channel", "CHANNEL" }, { "message", " MESSAGE" }, { "ts", null } - }; - var actual = spySlackAPI.Arguments; - - Assert.That(actual[0], Is.EqualTo(expected1)); - } - - [Test] - [Category("IgnoreCI")] // Reason: screen capture is not work run on batchmode - public async Task HandleLog_MultiChannel_PostToSlack() - { - var settings = CreateEmptyAutopilotSettings(); - var spySlackAPI = new SpySlackAPI(); - var reporter = new SlackReporter(settings, spySlackAPI); - var sut = new LogMessageHandler(settings, reporter); - - settings.slackToken = "TOKEN"; - settings.slackChannels = "CHANNEL1,CHANNEL2"; - settings.mentionSubTeamIDs = string.Empty; - settings.addHereInSlackMessage = false; - settings.handleException = true; - sut.HandleLog("MESSAGE", "STACKTRACE", LogType.Exception); - await UniTask.DelayFrame(8); - - var expected1 = new Dictionary - { - { "token", "TOKEN" }, { "channel", "CHANNEL1" }, { "message", "MESSAGE" }, { "ts", null } - }; - var expected2 = new Dictionary - { - { "token", "TOKEN" }, { "channel", "CHANNEL1" }, { "message", "IMAGE" }, { "ts", "1" } - }; - var expected3 = new Dictionary - { - { "token", "TOKEN" }, - { "channel", "CHANNEL1" }, - { "message", "MESSAGE\n\n```STACKTRACE```" }, - { "ts", "1" } - }; - var expected4 = new Dictionary - { - { "token", "TOKEN" }, { "channel", "CHANNEL2" }, { "message", "MESSAGE" }, { "ts", null } - }; - var expected5 = new Dictionary - { - { "token", "TOKEN" }, { "channel", "CHANNEL2" }, { "message", "IMAGE" }, { "ts", "1" } - }; - var expected6 = new Dictionary - { - { "token", "TOKEN" }, - { "channel", "CHANNEL2" }, - { "message", "MESSAGE\n\n```STACKTRACE```" }, - { "ts", "1" } - }; - var expected = new List> - { - expected1, - expected2, - expected3, - expected4, - expected5, - expected6 - }; - var actual = spySlackAPI.Arguments; - - Assert.That(actual, Is.EqualTo(expected)); + Assert.That(spyReporter.Arguments, Is.Empty); } private static AutopilotSettings CreateEmptyAutopilotSettings() { var settings = ScriptableObject.CreateInstance(); - settings.slackToken = "dummy token"; - settings.slackChannels = "dummy channel"; - settings.mentionSubTeamIDs = string.Empty; settings.ignoreMessages = new string[] { }; return settings; } diff --git a/Tests/TestAssets/AutopilotSettingsForImmediatlyFacingErrors.asset b/Tests/TestAssets/AutopilotSettingsForImmediatlyFacingErrors.asset new file mode 100644 index 0000000..51727c3 --- /dev/null +++ b/Tests/TestAssets/AutopilotSettingsForImmediatlyFacingErrors.asset @@ -0,0 +1,32 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c7a1731caa574ed4b9334b7d52febe27, type: 3} + m_Name: AutopilotSettingsForImmediatlyFacingErrors + m_EditorClassIdentifier: + description: + sceneAgentMaps: [] + fallbackAgent: {fileID: 0} + observerAgent: {fileID: 0} + lifespanSec: 300 + randomSeed: + timeScale: 1 + junitReportPath: + slackToken: + slackChannels: + mentionSubTeamIDs: + addHereInSlackMessage: 0 + handleException: 1 + handleError: 1 + handleAssert: 1 + handleWarning: 0 + ignoreMessages: [] + reporter: {fileID: 0} diff --git a/Tests/TestAssets/AutopilotSettingsForImmediatlyFacingErrors.asset.meta b/Tests/TestAssets/AutopilotSettingsForImmediatlyFacingErrors.asset.meta new file mode 100644 index 0000000..43a69ed --- /dev/null +++ b/Tests/TestAssets/AutopilotSettingsForImmediatlyFacingErrors.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0d887db4a0ca470f845d1c3d0236d31 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: