From 6fa504ec3f34237f61f985a06c58f99ce1d9a25d Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Wed, 8 May 2024 23:19:02 +0900 Subject: [PATCH 1/7] Upgrade test-helper package to v0.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9953dab..49a5c05 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "changelogUrl": "https://github.com/nowsprinting/test-helper.monkey/releases", "dependencies": { "com.cysharp.unitask": "2.3.3", - "com.nowsprinting.test-helper": "0.6.0", + "com.nowsprinting.test-helper": "0.7.1", "com.nowsprinting.test-helper.random": "0.3.0", "com.unity.ugui": "1.0.0" }, From a6b93bc9810314966332a81ccd58c37f2e3f68c4 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Thu, 9 May 2024 09:36:56 +0900 Subject: [PATCH 2/7] Mod unified operates logging includes screenshot filename --- Runtime/Monkey.cs | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/Runtime/Monkey.cs b/Runtime/Monkey.cs index 30b3943..80a484d 100644 --- a/Runtime/Monkey.cs +++ b/Runtime/Monkey.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using Cysharp.Threading.Tasks; using TestHelper.Monkey.Annotations; @@ -87,10 +88,6 @@ public static async UniTask Run(MonkeyConfig config, CancellationToken cancellat } } - private class CoroutineRunner : MonoBehaviour - { - } - /// /// Run a step of monkey testing. /// @@ -116,23 +113,16 @@ public static async UniTask RunStep( return false; } + var message = new StringBuilder($"{selectedOperator} operates to {selectedComponent.gameObject.name}"); if (screenshotOptions != null) { - if (s_coroutineRunner == null || (bool)s_coroutineRunner == false) - { - s_coroutineRunner = new GameObject("CoroutineRunner").AddComponent(); - } - - await ScreenshotHelper.TakeScreenshot( - directory: screenshotOptions.Directory, - filename: screenshotOptions.FilenameStrategy.GetFilename(), - superSize: screenshotOptions.SuperSize, - stereoCaptureMode: screenshotOptions.StereoCaptureMode - ) - .ToUniTask(s_coroutineRunner); + var filename = screenshotOptions.FilenameStrategy.GetFilename(); + await TakeScreenshotAsync(screenshotOptions, filename); + message.Append($" ({filename})"); } - logger.Log($"{selectedOperator} operates to {selectedComponent.gameObject.name}"); + logger.Log(message.ToString()); + await selectedOperator.OperateAsync(selectedComponent, cancellationToken); return true; } @@ -159,5 +149,26 @@ internal static (Component, IOperator) LotteryOperator(List<(Component, IOperato return (null, null); } + + private static async UniTask TakeScreenshotAsync(ScreenshotOptions screenshotOptions, string filename) + { + if (s_coroutineRunner == null || (bool)s_coroutineRunner == false) + { + s_coroutineRunner = new GameObject("CoroutineRunner").AddComponent(); + } + + await ScreenshotHelper.TakeScreenshot( + directory: screenshotOptions.Directory, + filename: filename, + superSize: screenshotOptions.SuperSize, + stereoCaptureMode: screenshotOptions.StereoCaptureMode, + logFilepath: false + ) + .ToUniTask(s_coroutineRunner); + } + + private class CoroutineRunner : MonoBehaviour + { + } } } From 09d1dcac1d834c41a53b116e8678403a42e61a92 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Sat, 11 May 2024 17:35:26 +0900 Subject: [PATCH 3/7] Refactor tests --- Tests/Runtime/MonkeyTest.cs | 164 +++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 75 deletions(-) diff --git a/Tests/Runtime/MonkeyTest.cs b/Tests/Runtime/MonkeyTest.cs index f5df96f..1ab3798 100644 --- a/Tests/Runtime/MonkeyTest.cs +++ b/Tests/Runtime/MonkeyTest.cs @@ -19,6 +19,8 @@ using UnityEngine; using AssertionException = UnityEngine.Assertions.AssertionException; +// ReSharper disable MethodSupportsCancellation + namespace TestHelper.Monkey { [TestFixture] @@ -26,29 +28,33 @@ public class MonkeyTest { private const string TestScene = "Packages/com.nowsprinting.test-helper.monkey/Tests/Scenes/Operators.unity"; - private readonly IEnumerable _operators = new IOperator[] + private IEnumerable _operators; + private InteractiveComponentCollector _interactiveComponentCollector; + + [SetUp] + public void SetUp() { - new UGUIClickOperator(), // click - new UGUIClickAndHoldOperator(1), // click and hold 1ms - new UGUITextInputOperator() - }; + _operators = new IOperator[] + { + new UGUIClickOperator(), // click + new UGUIClickAndHoldOperator(1), // click and hold 1ms + new UGUITextInputOperator() + }; + _interactiveComponentCollector = new InteractiveComponentCollector(operators: _operators); + } [Test] [LoadScene(TestScene)] public async Task RunStep_finish() { - var config = new MonkeyConfig - { - DelayMillis = 1, // 1ms - }; - - var interactiveComponentCollector = new InteractiveComponentCollector(operators: _operators); + var config = new MonkeyConfig(); var didAct = await Monkey.RunStep( config.Random, config.Logger, config.Screenshots, config.IsReachable, - interactiveComponentCollector); + _interactiveComponentCollector); + Assert.That(didAct, Is.EqualTo(true)); } @@ -56,23 +62,19 @@ public async Task RunStep_finish() [LoadScene(TestScene)] public async Task RunStep_noInteractiveComponent_abort() { - var interactiveComponentCollector = new InteractiveComponentCollector(operators: _operators); - foreach (var component in interactiveComponentCollector.FindInteractableComponents()) + foreach (var component in _interactiveComponentCollector.FindInteractableComponents()) { component.gameObject.SetActive(false); } - var config = new MonkeyConfig - { - DelayMillis = 1, // 1ms - }; - + var config = new MonkeyConfig(); var didAct = await Monkey.RunStep( config.Random, config.Logger, config.Screenshots, config.IsReachable, - interactiveComponentCollector); + _interactiveComponentCollector); + Assert.That(didAct, Is.EqualTo(false)); } @@ -116,8 +118,7 @@ public async Task Run_cancel() [LoadScene(TestScene)] public async Task Run_noInteractiveComponent_abort() { - var interactiveComponentCollector = new InteractiveComponentCollector(); - foreach (var component in interactiveComponentCollector.FindInteractableComponents()) + foreach (var component in _interactiveComponentCollector.FindInteractableComponents()) { component.gameObject.SetActive(false); } @@ -143,8 +144,7 @@ public async Task Run_noInteractiveComponent_abort() [LoadScene(TestScene)] public async Task Run_noInteractiveComponentAndSecondsToErrorForNoInteractiveComponentIsZero_finish() { - var interactiveComponentCollector = new InteractiveComponentCollector(); - foreach (var component in interactiveComponentCollector.FindInteractableComponents()) + foreach (var component in _interactiveComponentCollector.FindInteractableComponents()) { component.gameObject.SetActive(false); } @@ -205,8 +205,7 @@ public async Task Run_withGizmos_showGizmosAndReverted() [LoadScene(TestScene)] public void GetOperators_GotAllInteractableComponentAndOperators() { - var interactiveComponentCollector = new InteractiveComponentCollector(operators: _operators); - var operators = Monkey.GetOperators(interactiveComponentCollector); + var operators = Monkey.GetOperators(_interactiveComponentCollector); var actual = new List(); foreach (var (component, @operator) in operators) { @@ -237,8 +236,7 @@ public void GetOperators_WithIgnoreAnnotation_Excluded() { GameObject.Find("UsingOnPointerClickHandler").AddComponent(); - var interactiveComponentCollector = new InteractiveComponentCollector(operators: _operators); - var operators = Monkey.GetOperators(interactiveComponentCollector); + var operators = Monkey.GetOperators(_interactiveComponentCollector); var actual = new List(); foreach (var (component, _) in operators) { @@ -300,38 +298,58 @@ public void LotteryOperator_BingoReachableComponent_ReturnOperator() [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] public class Screenshots { + private IEnumerable _operators; + private InteractiveComponentCollector _interactiveComponentCollector; + private const int FileSizeThreshold = 5441; // VGA size solid color file size private const int FileSizeThreshold2X = 100 * 1024; // Normal size is 80 to 90KB private readonly string _defaultOutputDirectory = CommandLineArgs.GetScreenshotDirectory(); + private string _filename; + private string _path; - [Test] - [LoadScene(TestScene)] - public async Task Run_withScreenshots_takeScreenshotsAndSaveToDefaultPath() + [SetUp] + public void SetUp() { - var filename = $"{nameof(Run_withScreenshots_takeScreenshotsAndSaveToDefaultPath)}_0001.png"; - var path = Path.Combine(_defaultOutputDirectory, filename); - if (File.Exists(path)) + _operators = new IOperator[] { - File.Delete(path); - } + new UGUIClickOperator(), // click + new UGUIClickAndHoldOperator(1), // click and hold 1ms + new UGUITextInputOperator() + }; + _interactiveComponentCollector = new InteractiveComponentCollector(operators: _operators); - Assume.That(path, Does.Not.Exist); + _filename = $"{TestContext.CurrentContext.Test.Name}_0001.png"; + _path = Path.Combine(_defaultOutputDirectory, _filename); + if (File.Exists(_path)) + { + File.Delete(_path); + } + } + + [Test] + [LoadScene(TestScene)] + public async Task RunStep_withScreenshots_takeScreenshotsAndSaveToDefaultPath() + { var config = new MonkeyConfig { - Lifetime = TimeSpan.FromMilliseconds(200), // 200ms - DelayMillis = 1, // 1ms - Screenshots = new ScreenshotOptions() // take screenshots and save files + Screenshots = new ScreenshotOptions(), // take screenshots and save files, }; - await Monkey.Run(config); - Assert.That(path, Does.Exist); - Assert.That(new FileInfo(path), Has.Length.GreaterThan(FileSizeThreshold)); + await Monkey.RunStep( + config.Random, + config.Logger, + config.Screenshots, + config.IsReachable, + _interactiveComponentCollector); + + Assert.That(_path, Does.Exist); + Assert.That(new FileInfo(_path), Has.Length.GreaterThan(FileSizeThreshold)); } [Test] [LoadScene(TestScene)] - public async Task Run_withScreenshots_specifyPath_takeScreenshotsAndSaveToSpecifiedPath() + public async Task RunStep_withScreenshots_specifyPath_takeScreenshotsAndSaveToSpecifiedPath() { var relativeDirectory = Path.Combine("Logs", "TestHelper.Monkey", "SpecifiedPath"); var filenamePrefix = "Run_withScreenshots_specifyPath"; @@ -342,20 +360,22 @@ public async Task Run_withScreenshots_specifyPath_takeScreenshotsAndSaveToSpecif File.Delete(path); } - Assume.That(path, Does.Not.Exist); - var config = new MonkeyConfig { - Lifetime = TimeSpan.FromMilliseconds(200), // 200ms - DelayMillis = 1, // 1ms - Screenshots = new ScreenshotOptions() + Screenshots = new ScreenshotOptions { Directory = relativeDirectory, FilenameStrategy = new StubScreenshotFilenameStrategy(filename), SuperSize = 2, }, }; - await Monkey.Run(config); + + await Monkey.RunStep( + config.Random, + config.Logger, + config.Screenshots, + config.IsReachable, + _interactiveComponentCollector); Assert.That(path, Does.Exist); Assert.That(new FileInfo(path), Has.Length.GreaterThan(FileSizeThreshold)); @@ -364,17 +384,8 @@ public async Task Run_withScreenshots_specifyPath_takeScreenshotsAndSaveToSpecif [Test] [Description("This test fails with stereo rendering settings.")] [LoadScene(TestScene)] - public async Task Run_withScreenshots_superSize_takeScreenshotsSuperSize() + public async Task RunStep_withScreenshots_superSize_takeScreenshotsSuperSize() { - var filename = $"{nameof(Run_withScreenshots_superSize_takeScreenshotsSuperSize)}_0001.png"; - var path = Path.Combine(_defaultOutputDirectory, filename); - if (File.Exists(path)) - { - File.Delete(path); - } - - Assume.That(path, Does.Not.Exist); - var config = new MonkeyConfig { Lifetime = TimeSpan.FromMilliseconds(200), // 200ms @@ -384,10 +395,16 @@ public async Task Run_withScreenshots_superSize_takeScreenshotsSuperSize() SuperSize = 2, // 2x size }, }; - await Monkey.Run(config); - Assert.That(path, Does.Exist); - Assert.That(new FileInfo(path), Has.Length.GreaterThan(FileSizeThreshold2X)); + await Monkey.RunStep( + config.Random, + config.Logger, + config.Screenshots, + config.IsReachable, + _interactiveComponentCollector); + + Assert.That(_path, Does.Exist); + Assert.That(new FileInfo(_path), Has.Length.GreaterThan(FileSizeThreshold2X)); // Note: This test fails with stereo rendering settings. // See: https://docs.unity3d.com/Manual/SinglePassStereoRendering.html } @@ -395,17 +412,8 @@ public async Task Run_withScreenshots_superSize_takeScreenshotsSuperSize() [Test] [LoadScene(TestScene)] [Description("Is it a stereo screenshot? See for yourself! Be a witness!!")] - public async Task Run_withScreenshots_stereo_takeScreenshotsStereo() + public async Task RunStep_withScreenshots_stereo_takeScreenshotsStereo() { - var filename = $"{nameof(Run_withScreenshots_stereo_takeScreenshotsStereo)}_0001.png"; - var path = Path.Combine(_defaultOutputDirectory, filename); - if (File.Exists(path)) - { - File.Delete(path); - } - - Assume.That(path, Does.Not.Exist); - var config = new MonkeyConfig { Lifetime = TimeSpan.FromMilliseconds(200), // 200ms @@ -415,9 +423,15 @@ public async Task Run_withScreenshots_stereo_takeScreenshotsStereo() StereoCaptureMode = ScreenCapture.StereoScreenCaptureMode.BothEyes, }, }; - await Monkey.Run(config); - Assert.That(path, Does.Exist); + await Monkey.RunStep( + config.Random, + config.Logger, + config.Screenshots, + config.IsReachable, + _interactiveComponentCollector); + + Assert.That(_path, Does.Exist); // Note: Require stereo rendering settings. // See: https://docs.unity3d.com/Manual/SinglePassStereoRendering.html } From 4dd192fed4d94181e9e9b2eb0eba12e355298e75 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Sat, 11 May 2024 16:26:25 +0900 Subject: [PATCH 4/7] Add take screenshot when over SecondsToErrorForNoInteractiveComponent And change throws exception type to TimeoutException --- Runtime/Monkey.cs | 18 +++++++++++------ Tests/Runtime/MonkeyTest.cs | 39 ++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/Runtime/Monkey.cs b/Runtime/Monkey.cs index 80a484d..ce66797 100644 --- a/Runtime/Monkey.cs +++ b/Runtime/Monkey.cs @@ -13,7 +13,6 @@ using TestHelper.Random; using TestHelper.RuntimeInternals; using UnityEngine; -using UnityEngine.Assertions; using UnityEngine.EventSystems; namespace TestHelper.Monkey @@ -68,12 +67,19 @@ public static async UniTask Run(MonkeyConfig config, CancellationToken cancellat { lastOperationTime = Time.time; } - else if (config.SecondsToErrorForNoInteractiveComponent > 0) + else if (0 < config.SecondsToErrorForNoInteractiveComponent && + config.SecondsToErrorForNoInteractiveComponent < (Time.time - lastOperationTime)) { - Assert.IsTrue( - (Time.time - lastOperationTime) < config.SecondsToErrorForNoInteractiveComponent, - $"Interactive component not found in {config.SecondsToErrorForNoInteractiveComponent} seconds" - ); + var message = new StringBuilder( + $"Interactive component not found in {config.SecondsToErrorForNoInteractiveComponent} seconds"); + if (config.Screenshots != null) + { + var filename = config.Screenshots.FilenameStrategy.GetFilename(); + await TakeScreenshotAsync(config.Screenshots, filename); + message.Append($" ({filename})"); + } + + throw new TimeoutException(message.ToString()); } await UniTask.Delay(config.DelayMillis, DelayType.DeltaTime, cancellationToken: cancellationToken); diff --git a/Tests/Runtime/MonkeyTest.cs b/Tests/Runtime/MonkeyTest.cs index 1ab3798..e8aca65 100644 --- a/Tests/Runtime/MonkeyTest.cs +++ b/Tests/Runtime/MonkeyTest.cs @@ -17,7 +17,6 @@ using TestHelper.Random; using TestHelper.RuntimeInternals; using UnityEngine; -using AssertionException = UnityEngine.Assertions.AssertionException; // ReSharper disable MethodSupportsCancellation @@ -116,7 +115,7 @@ public async Task Run_cancel() [Test] [LoadScene(TestScene)] - public async Task Run_noInteractiveComponent_abort() + public async Task Run_noInteractiveComponent_throwTimeoutException() { foreach (var component in _interactiveComponentCollector.FindInteractableComponents()) { @@ -125,16 +124,16 @@ public async Task Run_noInteractiveComponent_abort() var config = new MonkeyConfig { - Lifetime = TimeSpan.FromSeconds(5), // 5sec + Lifetime = TimeSpan.FromSeconds(2), // 2sec SecondsToErrorForNoInteractiveComponent = 1, // 1sec }; try { await Monkey.Run(config); - Assert.Fail("AssertionException was not thrown"); + Assert.Fail("TimeoutException was not thrown"); } - catch (AssertionException e) + catch (TimeoutException e) { Assert.That(e.Message, Does.Contain("Interactive component not found in 1 seconds")); } @@ -435,6 +434,36 @@ await Monkey.RunStep( // Note: Require stereo rendering settings. // See: https://docs.unity3d.com/Manual/SinglePassStereoRendering.html } + + [Test] + [LoadScene(TestScene)] + public async Task Run_withScreenshots_noInteractiveComponent_takeScreenshot() + { + var interactiveComponentCollector = new InteractiveComponentCollector(); + foreach (var component in interactiveComponentCollector.FindInteractableComponents()) + { + component.gameObject.SetActive(false); + } + + var config = new MonkeyConfig + { + Lifetime = TimeSpan.FromSeconds(2), // 2sec + SecondsToErrorForNoInteractiveComponent = 1, // 1sec + Screenshots = new ScreenshotOptions() // take screenshots and save files + }; + + try + { + await Monkey.Run(config); + Assert.Fail("TimeoutException was not thrown"); + } + catch (TimeoutException e) + { + Assert.That(e.Message, Does.Contain($"Interactive component not found in 1 seconds ({_filename})")); + } + + Assert.That(_path, Does.Exist); + } } } } From 36a062263755df796b241977f91cbadf1f5ab2fa Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Sun, 12 May 2024 09:38:04 +0900 Subject: [PATCH 5/7] Add verbose option --- .../DefaultComponentInteractableStrategy.cs | 2 +- .../DefaultReachableStrategy.cs | 43 ++++- Runtime/Extensions/GameObjectExtensions.cs | 5 +- Runtime/GameObjectFinder.cs | 6 +- Runtime/Hints/InteractiveComponentHint.cs | 2 +- Runtime/InteractiveComponent.cs | 10 +- Runtime/InteractiveComponentCollector.cs | 6 +- Runtime/Monkey.cs | 68 ++++++-- Runtime/MonkeyConfig.cs | 9 +- Tests/Performance/MonkeyTest.cs | 9 +- ...TestHelper.Monkey.Performance.Tests.asmdef | 2 +- .../Annotations/PositionAnnotationTest.cs | 2 +- Tests/Runtime/DefaultStrategies.meta | 3 + .../DefaultReachableStrategyTest.cs | 147 ++++++++++++++++++ .../DefaultReachableStrategyTest.cs.meta | 3 + .../Extensions/GameObjectExtensionsTest.cs | 8 +- Tests/Runtime/MonkeyTest.cs | 94 +++++++++-- 17 files changed, 368 insertions(+), 51 deletions(-) create mode 100644 Tests/Runtime/DefaultStrategies.meta create mode 100644 Tests/Runtime/DefaultStrategies/DefaultReachableStrategyTest.cs create mode 100644 Tests/Runtime/DefaultStrategies/DefaultReachableStrategyTest.cs.meta diff --git a/Runtime/DefaultStrategies/DefaultComponentInteractableStrategy.cs b/Runtime/DefaultStrategies/DefaultComponentInteractableStrategy.cs index 2198932..5263606 100644 --- a/Runtime/DefaultStrategies/DefaultComponentInteractableStrategy.cs +++ b/Runtime/DefaultStrategies/DefaultComponentInteractableStrategy.cs @@ -26,7 +26,7 @@ public static bool IsInteractable(Component component) { // UI element var selectable = component as Selectable; - if (selectable != null) + if (selectable) { return selectable.interactable; } diff --git a/Runtime/DefaultStrategies/DefaultReachableStrategy.cs b/Runtime/DefaultStrategies/DefaultReachableStrategy.cs index 001ec7b..959b34f 100644 --- a/Runtime/DefaultStrategies/DefaultReachableStrategy.cs +++ b/Runtime/DefaultStrategies/DefaultReachableStrategy.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Text; using TestHelper.Monkey.ScreenPointStrategies; using UnityEngine; using UnityEngine.EventSystems; +using Object = UnityEngine.Object; namespace TestHelper.Monkey.DefaultStrategies { @@ -23,10 +25,12 @@ public static class DefaultReachableStrategy /// /// Specify if avoid GC memory allocation /// Specify if avoid GC memory allocation + /// Output verbose log if need /// True if this GameObject is reachable from user public static bool IsReachable(GameObject gameObject, PointerEventData eventData = null, - List results = null) + List results = null, + ILogger verboseLogger = null) { eventData = eventData ?? new PointerEventData(EventSystem.current); eventData.position = GetScreenPoint.Invoke(gameObject); @@ -35,12 +39,38 @@ public static bool IsReachable(GameObject gameObject, results.Clear(); EventSystem.current.RaycastAll(eventData, results); - return results.Count > 0 && IsSameOrChildObject(gameObject, results[0].gameObject.transform); + if (results.Count == 0) + { + if (verboseLogger != null) + { + var message = new StringBuilder(CreateMessage(gameObject, eventData.position)); + message.Append(" Raycast is not hit."); + verboseLogger.Log(message.ToString()); + } + + return false; + } + + var isSameOrChildObject = IsSameOrChildObject(gameObject, results[0].gameObject.transform); + if (!isSameOrChildObject && verboseLogger != null) + { + var message = new StringBuilder(CreateMessage(gameObject, eventData.position)); + message.Append(" Raycast hit other objects: "); + foreach (var result in results) + { + message.Append(result.gameObject.name); + message.Append(", "); + } + + verboseLogger.Log(message.ToString(0, message.Length - 2)); + } + + return isSameOrChildObject; } private static bool IsSameOrChildObject(GameObject target, Transform hitObjectTransform) { - while (hitObjectTransform != null) + while (hitObjectTransform) { if (hitObjectTransform == target.transform) { @@ -52,5 +82,12 @@ private static bool IsSameOrChildObject(GameObject target, Transform hitObjectTr return false; } + + private static string CreateMessage(Object gameObject, Vector2 position) + { + var x = (int)position.x; + var y = (int)position.y; + return $"Not reachable to {gameObject.name}({gameObject.GetInstanceID()}), position=({x},{y})."; + } } } diff --git a/Runtime/Extensions/GameObjectExtensions.cs b/Runtime/Extensions/GameObjectExtensions.cs index 1f62164..d665eef 100644 --- a/Runtime/Extensions/GameObjectExtensions.cs +++ b/Runtime/Extensions/GameObjectExtensions.cs @@ -68,8 +68,9 @@ public static Camera GetAssociatedCamera(this GameObject gameObject) /// Specify if avoid GC memory allocation /// Specify if avoid GC memory allocation /// true: this object can control by user + [Obsolete("Use DefaultReachableStrategy instead.")] public static bool IsReachable(this GameObject gameObject, - Func, bool> isReachable = null, + Func, ILogger, bool> isReachable = null, PointerEventData pointerEventData = null, List raycastResults = null) { @@ -80,7 +81,7 @@ public static bool IsReachable(this GameObject gameObject, raycastResults = raycastResults ?? new List(); raycastResults.Clear(); - return isReachable.Invoke(gameObject, pointerEventData, raycastResults); + return isReachable.Invoke(gameObject, pointerEventData, raycastResults, null); } /// diff --git a/Runtime/GameObjectFinder.cs b/Runtime/GameObjectFinder.cs index acb478b..a18ac6c 100644 --- a/Runtime/GameObjectFinder.cs +++ b/Runtime/GameObjectFinder.cs @@ -20,7 +20,7 @@ namespace TestHelper.Monkey public class GameObjectFinder { private readonly double _timeoutSeconds; - private readonly Func, bool> _isReachable; + private readonly Func, ILogger, bool> _isReachable; private readonly Func _isComponentInteractable; private readonly PointerEventData _eventData = new PointerEventData(EventSystem.current); private readonly List _results = new List(); @@ -36,7 +36,7 @@ public class GameObjectFinder /// The function returns the Component is interactable or not. /// Default is DefaultComponentInteractableStrategy.IsInteractable. public GameObjectFinder(double timeoutSeconds = 1.0d, - Func, bool> isReachable = null, + Func, ILogger, bool> isReachable = null, Func isComponentInteractable = null) { Assert.IsTrue(timeoutSeconds > MinTimeoutSeconds, @@ -71,7 +71,7 @@ private enum Reason return (null, Reason.NotMatchPath); } - if (reachable && !_isReachable.Invoke(foundObject, _eventData, _results)) + if (reachable && !_isReachable.Invoke(foundObject, _eventData, _results, null)) { return (null, Reason.NotReachable); } diff --git a/Runtime/Hints/InteractiveComponentHint.cs b/Runtime/Hints/InteractiveComponentHint.cs index ff45485..3292a89 100644 --- a/Runtime/Hints/InteractiveComponentHint.cs +++ b/Runtime/Hints/InteractiveComponentHint.cs @@ -102,7 +102,7 @@ private void Refresh() var interactiveComponentCollector = new InteractiveComponentCollector(); foreach (var component in interactiveComponentCollector.FindInteractableComponents()) { - var dst = component.gameObject.IsReachable(isReachable: DefaultReachableStrategy.IsReachable) + var dst = DefaultReachableStrategy.IsReachable(component.gameObject) ? _tmpReallyInteractives : _tmpNotReallyInteractives; diff --git a/Runtime/InteractiveComponent.cs b/Runtime/InteractiveComponent.cs index b0378c8..1a630ad 100644 --- a/Runtime/InteractiveComponent.cs +++ b/Runtime/InteractiveComponent.cs @@ -39,13 +39,13 @@ public class InteractiveComponent [SuppressMessage("ReSharper", "InconsistentNaming")] public GameObject gameObject => component.gameObject; - private readonly Func, bool> _isReachable; + private readonly Func, ILogger, bool> _isReachable; private readonly IEnumerable _operators; private readonly PointerEventData _eventData = new PointerEventData(EventSystem.current); private readonly List _results = new List(); private InteractiveComponent(MonoBehaviour component, - Func, bool> isReachable = null, + Func, ILogger, bool> isReachable = null, IEnumerable operators = null) { this.component = component; @@ -64,7 +64,7 @@ private InteractiveComponent(MonoBehaviour component, /// All available operators in autopilot/tests. Usually defined in MonkeyConfig /// Returns new InteractableComponent instance from MonoBehaviour. If MonoBehaviour is not interactable so, return null. public static InteractiveComponent CreateInteractableComponent(MonoBehaviour component, - Func, bool> isReachable = null, + Func, ILogger, bool> isReachable = null, Func isComponentInteractable = null, IEnumerable operators = null) { @@ -90,7 +90,7 @@ public static InteractiveComponent CreateInteractableComponent(MonoBehaviour com /// Returns new InteractableComponent instance from GameObject. If GameObject is not interactable so, return null. [Obsolete("Obsolete due to non-deterministic behavior when GameObject has multiple interactable components.")] public static InteractiveComponent CreateInteractableComponent(GameObject gameObject, - Func, bool> isReachable = null, + Func, ILogger, bool> isReachable = null, Func isComponentInteractable = null, IEnumerable operators = null) { @@ -128,7 +128,7 @@ public bool IsReallyInteractiveFromUser(Func screenPointStr [Obsolete("Use GameObjectExtensions.IsReachable() instead")] public bool IsReachable() { - return gameObject.IsReachable(_isReachable, _eventData, _results); + return _isReachable(gameObject, _eventData, _results, null); } /// diff --git a/Runtime/InteractiveComponentCollector.cs b/Runtime/InteractiveComponentCollector.cs index 51bc27c..edc7b19 100644 --- a/Runtime/InteractiveComponentCollector.cs +++ b/Runtime/InteractiveComponentCollector.cs @@ -19,7 +19,7 @@ namespace TestHelper.Monkey // TODO: Rename to InteractableComponentsFinder public class InteractiveComponentCollector { - private readonly Func, bool> _isReachable; + private readonly Func, ILogger, bool> _isReachable; private readonly Func _isInteractable; private readonly IEnumerable _operators; private readonly PointerEventData _eventData = new PointerEventData(EventSystem.current); @@ -34,7 +34,7 @@ public class InteractiveComponentCollector /// Default is DefaultComponentInteractableStrategy.IsInteractable. /// All available operators in autopilot/tests. Usually defined in MonkeyConfig public InteractiveComponentCollector( - Func, bool> isReachable = null, + Func, ILogger, bool> isReachable = null, Func isInteractable = null, IEnumerable operators = null) { @@ -87,7 +87,7 @@ public IEnumerable FindReachableInteractableComponents() { foreach (var interactableComponent in FindInteractableComponents()) { - if (_isReachable.Invoke(interactableComponent.gameObject, _eventData, _results)) + if (_isReachable.Invoke(interactableComponent.gameObject, _eventData, _results, null)) { yield return interactableComponent; } diff --git a/Runtime/Monkey.cs b/Runtime/Monkey.cs index ce66797..c52a094 100644 --- a/Runtime/Monkey.cs +++ b/Runtime/Monkey.cs @@ -8,7 +8,6 @@ using System.Threading; using Cysharp.Threading.Tasks; using TestHelper.Monkey.Annotations; -using TestHelper.Monkey.Extensions; using TestHelper.Monkey.Operators; using TestHelper.Random; using TestHelper.RuntimeInternals; @@ -62,6 +61,7 @@ public static async UniTask Run(MonkeyConfig config, CancellationToken cancellat config.Screenshots, config.IsReachable, interactableComponentCollector, + config.Verbose, cancellationToken); if (didAct) { @@ -102,18 +102,21 @@ public static async UniTask Run(MonkeyConfig config, CancellationToken cancellat /// Take screenshots options from MonkeyConfig /// Function returns the GameObject is reachable from user or not. from MonkeyConfig /// InteractableComponentCollector instance includes isReachable, isInteractable, and operators + /// Output verbose logs /// Cancellation token /// True if any operator was executed public static async UniTask RunStep( IRandom random, ILogger logger, ScreenshotOptions screenshotOptions, - Func, bool> isReachable, + Func, ILogger, bool> isReachable, InteractiveComponentCollector interactableComponentCollector, + bool verbose = false, CancellationToken cancellationToken = default) { - var operators = GetOperators(interactableComponentCollector); - var (selectedComponent, selectedOperator) = LotteryOperator(operators.ToList(), random, isReachable); + var lotteryEntries = GetLotteryEntries(interactableComponentCollector, verbose ? logger : null); + var (selectedComponent, selectedOperator) = LotteryOperator(lotteryEntries.ToList(), random, isReachable, + verbose ? logger : null); if (selectedComponent == null || selectedOperator == null) { return false; @@ -133,19 +136,61 @@ public static async UniTask RunStep( return true; } - internal static IEnumerable<(Component, IOperator)> GetOperators(InteractiveComponentCollector collector) + internal static IEnumerable<(Component, IOperator)> GetLotteryEntries(InteractiveComponentCollector collector, + ILogger verboseLogger = null) { - return collector.FindInteractableComponentsAndOperators() - .Where(x => !x.Item1.gameObject.TryGetComponent(typeof(IgnoreAnnotation), out _)); + var dictionary = verboseLogger != null ? new Dictionary() : null; + + foreach (var (component, iOperator) in collector.FindInteractableComponentsAndOperators()) + { + if (component.gameObject.TryGetComponent(typeof(IgnoreAnnotation), out _)) + { + dictionary?.TryAdd(component.gameObject, "Ignored"); + continue; + } + + dictionary?.TryAdd(component.gameObject, null); + yield return (component, iOperator); + } + + if (verboseLogger != null) + { + if (dictionary.Count == 0) + { + verboseLogger.Log("No lottery entries."); + } + else + { + var builder = new StringBuilder("Lottery entries: "); + foreach (var gameObject in dictionary.Keys) + { + var value = dictionary[gameObject]; + if (value != null) + { + builder.Append($"[{value}]"); + } + + builder.Append($"{gameObject.name}({gameObject.GetInstanceID()}), "); + } + + verboseLogger.Log(builder.ToString(0, builder.Length - 2)); + } + } } - internal static (Component, IOperator) LotteryOperator(List<(Component, IOperator)> operators, IRandom random, - Func, bool> isReachable) + internal static (Component, IOperator) LotteryOperator( + List<(Component, IOperator)> operators, + IRandom random, + Func, ILogger, bool> isReachable, + ILogger verboseLogger = null) { + var pointerEventData = new PointerEventData(EventSystem.current); + var raycastResults = new List(); + while (operators.Count > 0) { var (selectedComponent, selectedOperator) = operators[random.Next(operators.Count)]; - if (selectedComponent.gameObject.IsReachable(isReachable)) + if (isReachable(selectedComponent.gameObject, pointerEventData, raycastResults, verboseLogger)) { return (selectedComponent, selectedOperator); } @@ -153,12 +198,13 @@ internal static (Component, IOperator) LotteryOperator(List<(Component, IOperato operators.Remove((selectedComponent, selectedOperator)); } + verboseLogger?.Log("Lottery entries are empty or all of not reachable."); return (null, null); } private static async UniTask TakeScreenshotAsync(ScreenshotOptions screenshotOptions, string filename) { - if (s_coroutineRunner == null || (bool)s_coroutineRunner == false) + if (!s_coroutineRunner) { s_coroutineRunner = new GameObject("CoroutineRunner").AddComponent(); } diff --git a/Runtime/MonkeyConfig.cs b/Runtime/MonkeyConfig.cs index dc846f1..b8e9606 100644 --- a/Runtime/MonkeyConfig.cs +++ b/Runtime/MonkeyConfig.cs @@ -41,6 +41,11 @@ public class MonkeyConfig /// public ILogger Logger { get; set; } = Debug.unityLogger; + /// + /// Output verbose log if true + /// + public bool Verbose { get; set; } + /// /// Show Gizmos on GameView during running monkey test if true /// @@ -55,8 +60,8 @@ public class MonkeyConfig /// Function returns the GameObject is reachable from user or not. /// This function is include ScreenPointStrategy (GetScreenPoint function). /// - public Func, bool> - IsReachable { get; set; } = DefaultReachableStrategy.IsReachable; + public Func, ILogger, bool> IsReachable { get; set; } = + DefaultReachableStrategy.IsReachable; /// /// Function returns the Component is interactable or not. diff --git a/Tests/Performance/MonkeyTest.cs b/Tests/Performance/MonkeyTest.cs index 50cc843..27df100 100644 --- a/Tests/Performance/MonkeyTest.cs +++ b/Tests/Performance/MonkeyTest.cs @@ -6,11 +6,10 @@ using Cysharp.Threading.Tasks; using NUnit.Framework; using TestHelper.Attributes; -using TestHelper.Monkey; using Unity.PerformanceTesting; using UnityEngine.TestTools; -namespace Tests.Performance +namespace TestHelper.Monkey { public class MonkeyTest { @@ -19,14 +18,14 @@ public class MonkeyTest [Test] [Performance, Version(MeasurePackageVersion)] [LoadScene("../Scenes/Operators.unity")] - public void GetOperators_GotAllInteractableComponentAndOperators() + public void GetLotteryEntries_GotAllInteractableComponentAndOperators() { var config = new MonkeyConfig(); var interactableComponentCollector = new InteractiveComponentCollector(config); Measure.Method(() => { - Monkey.GetOperators(interactableComponentCollector); + Monkey.GetLotteryEntries(interactableComponentCollector); }) .WarmupCount(5) .MeasurementCount(20) @@ -41,7 +40,7 @@ public void LotteryOperator_BingoReachableComponent() { var config = new MonkeyConfig(); var interactableComponentCollector = new InteractiveComponentCollector(config); - var operators = Monkey.GetOperators(interactableComponentCollector); + var operators = Monkey.GetLotteryEntries(interactableComponentCollector); Measure.Method(() => { diff --git a/Tests/Performance/TestHelper.Monkey.Performance.Tests.asmdef b/Tests/Performance/TestHelper.Monkey.Performance.Tests.asmdef index 9876b6d..405eee7 100644 --- a/Tests/Performance/TestHelper.Monkey.Performance.Tests.asmdef +++ b/Tests/Performance/TestHelper.Monkey.Performance.Tests.asmdef @@ -1,6 +1,6 @@ { "name": "TestHelper.Monkey.Performance.Tests", - "rootNamespace": "", + "rootNamespace": "TestHelper.Monkey", "references": [ "UnityEngine.TestRunner", "UnityEditor.TestRunner", diff --git a/Tests/Runtime/Annotations/PositionAnnotationTest.cs b/Tests/Runtime/Annotations/PositionAnnotationTest.cs index 9970e3c..c99bd26 100644 --- a/Tests/Runtime/Annotations/PositionAnnotationTest.cs +++ b/Tests/Runtime/Annotations/PositionAnnotationTest.cs @@ -33,7 +33,7 @@ string name // Without no position annotations, IsReallyInteractiveFromUser() is always false because // gameObject.transform.position is not in the mesh. So IsReallyInteractiveFromUser() is true means // the position annotation work well - Assert.That(target.gameObject.IsReachable(DefaultReachableStrategy.IsReachable), Is.True); + Assert.That(DefaultReachableStrategy.IsReachable(target.gameObject), Is.True); } } } diff --git a/Tests/Runtime/DefaultStrategies.meta b/Tests/Runtime/DefaultStrategies.meta new file mode 100644 index 0000000..3ab7c1e --- /dev/null +++ b/Tests/Runtime/DefaultStrategies.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4d21b44b3e684217aa15af129b2a7c4b +timeCreated: 1715431551 \ No newline at end of file diff --git a/Tests/Runtime/DefaultStrategies/DefaultReachableStrategyTest.cs b/Tests/Runtime/DefaultStrategies/DefaultReachableStrategyTest.cs new file mode 100644 index 0000000..fe26781 --- /dev/null +++ b/Tests/Runtime/DefaultStrategies/DefaultReachableStrategyTest.cs @@ -0,0 +1,147 @@ +// Copyright (c) 2023-2024 Koji Hasegawa. +// This software is released under the MIT License. + +using System.Linq; // Do not remove, required for Unity 2022 or earlier +using System.Threading.Tasks; +using Cysharp.Threading.Tasks; // Do not remove, required for Unity 2022 or earlier +using NUnit.Framework; +using TestHelper.Attributes; +using TestHelper.Monkey.TestDoubles; +using TestHelper.RuntimeInternals; +using UnityEngine; +using UnityEngine.TestTools; + +namespace TestHelper.Monkey.DefaultStrategies +{ + [TestFixture] + public class DefaultReachableStrategyTest + { + [TestFixture(RenderMode.ScreenSpaceOverlay)] + [TestFixture(RenderMode.ScreenSpaceCamera)] + [TestFixture(RenderMode.WorldSpace)] + public class UI + { + private const string TestScenePath = "../../Scenes/GameObjectFinderUI.unity"; + private readonly GameObjectFinder _finder = new GameObjectFinder(0.1d); + private readonly RenderMode _canvasRenderMode; + + public UI(RenderMode canvasRenderMode) + { + _canvasRenderMode = canvasRenderMode; + } + + [SetUp] + public void SetUp() + { + var canvas = GameObject.Find("Canvas").GetComponent(); + canvas.renderMode = _canvasRenderMode; + if (_canvasRenderMode == RenderMode.WorldSpace) + { + canvas.worldCamera = Camera.main; + canvas.transform.position = new Vector3(0, 0, 500); + } + } + + [TestCase("ActiveText")] + [TestCase("Dialog")] // Child objects do not block raycast + [LoadScene(TestScenePath)] + public async Task IsReachable_Reachable(string target) + { + var gameObject = await _finder.FindByNameAsync(target, reachable: false); + Assert.That(DefaultReachableStrategy.IsReachable(gameObject), Is.True); + } + + [TestCase("OutOfSight")] + [TestCase("BehindTheWall")] + [LoadScene(TestScenePath)] + public async Task IsReachable_NotReachable(string target) + { + var gameObject = await _finder.FindByNameAsync(target, reachable: false); + Assert.That(DefaultReachableStrategy.IsReachable(gameObject), Is.False); + } + } + + [TestFixture("2D")] + [TestFixture("3D")] + [UnityPlatform(RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor, RuntimePlatform.LinuxEditor)] + public class Object + { + private readonly GameObjectFinder _finder = new GameObjectFinder(0.1d); + private readonly string _testScenePath; + + public Object(string dimension) + { + _testScenePath = $"../../Scenes/GameObjectFinder{dimension}.unity"; + } + + [SetUp] + public async Task SetUp() + { + await SceneManagerHelper.LoadSceneAsync(_testScenePath); + } + + [TestCase("NotInteractable")] + public async Task IsReachable_Reachable(string target) + { + var gameObject = await _finder.FindByNameAsync(target, reachable: false); + Assert.That(DefaultReachableStrategy.IsReachable(gameObject), Is.True); + } + + [TestCase("OutOfSight")] + [TestCase("BehindTheWall")] + public async Task IsReachable_NotReachable(string target) + { + var gameObject = await _finder.FindByNameAsync(target, reachable: false); + Assert.That(DefaultReachableStrategy.IsReachable(gameObject), Is.False); + } + } + + [TestFixture] + public class Verbose + { + private const string TestScenePath = "../../Scenes/GameObjectFinderUI.unity"; + private readonly GameObjectFinder _finder = new GameObjectFinder(0.1d); + + [TestCase("ActiveText")] + [TestCase("Dialog")] // Child objects do not block raycast + [LoadScene(TestScenePath)] + public async Task IsReachableWithVerbose_Reachable_NotOutputLog(string target) + { + var gameObject = await _finder.FindByNameAsync(target, reachable: false); + var spyLogger = new SpyLogger(); + var actual = DefaultReachableStrategy.IsReachable(gameObject, verboseLogger: spyLogger); + Assume.That(actual, Is.True); + + Assert.That(spyLogger.Messages, Is.Empty); + } + + [Test] + [LoadScene(TestScenePath)] + public async Task IsReachableWithVerbose_OutOfSight_LogVerboseNotHit() + { + var gameObject = await _finder.FindByNameAsync("OutOfSight", reachable: false); + var spyLogger = new SpyLogger(); + var actual = DefaultReachableStrategy.IsReachable(gameObject, verboseLogger: spyLogger); + Assume.That(actual, Is.False); + + Assert.That(spyLogger.Messages.Count, Is.EqualTo(1)); + Assert.That(spyLogger.Messages[0], Does.Match( + @"Not reachable to OutOfSight\(\d+\), position=\(\d+,\d+\)\. Raycast is not hit\.")); + } + + [Test] + [LoadScene(TestScenePath)] + public async Task IsReachableWithVerbose_BehindOtherObject_LogHitOtherObject() + { + var gameObject = await _finder.FindByNameAsync("BehindTheWall", reachable: false); + var spyLogger = new SpyLogger(); + var actual = DefaultReachableStrategy.IsReachable(gameObject, verboseLogger: spyLogger); + Assume.That(actual, Is.False); + + Assert.That(spyLogger.Messages.Count, Is.EqualTo(1)); + Assert.That(spyLogger.Messages[0], Does.Match( + @"Not reachable to BehindTheWall\(\d+\), position=\(\d+,\d+\)\. Raycast hit other objects: Wall, BehindTheWall")); + } + } + } +} diff --git a/Tests/Runtime/DefaultStrategies/DefaultReachableStrategyTest.cs.meta b/Tests/Runtime/DefaultStrategies/DefaultReachableStrategyTest.cs.meta new file mode 100644 index 0000000..20dd69f --- /dev/null +++ b/Tests/Runtime/DefaultStrategies/DefaultReachableStrategyTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6e1b9deca5534cff851a78c4be96b934 +timeCreated: 1715431561 \ No newline at end of file diff --git a/Tests/Runtime/Extensions/GameObjectExtensionsTest.cs b/Tests/Runtime/Extensions/GameObjectExtensionsTest.cs index fc1b4e9..8b9e094 100644 --- a/Tests/Runtime/Extensions/GameObjectExtensionsTest.cs +++ b/Tests/Runtime/Extensions/GameObjectExtensionsTest.cs @@ -56,7 +56,7 @@ public async Task IsReachable_Reachable(string target) { var actual = await _sut.FindByNameAsync(target, reachable: false); Assert.That(actual.IsReachable(DefaultReachableStrategy.IsReachable), Is.True); - // TODO: Remove argument after remove obsolete method + // TODO: Remove when remove obsolete method, Already copied to DefaultReachableStrategyTest. } [TestCase("OutOfSight")] @@ -66,7 +66,7 @@ public async Task IsReachable_NotReachable(string target) { var actual = await _sut.FindByNameAsync(target, reachable: false); Assert.That(actual.IsReachable(DefaultReachableStrategy.IsReachable), Is.False); - // TODO: Remove argument after remove obsolete method + // TODO: Remove when remove obsolete method, Already copied to DefaultReachableStrategyTest. } } @@ -98,7 +98,7 @@ public async Task IsReachable_Reachable(string target) { var actual = await _sut.FindByNameAsync(target, reachable: false); Assert.That(actual.IsReachable(DefaultReachableStrategy.IsReachable), Is.True); - // TODO: Remove argument after remove obsolete method + // TODO: Remove when remove obsolete method, Already copied to DefaultReachableStrategyTest. } [TestCase("OutOfSight")] @@ -107,7 +107,7 @@ public async Task IsReachable_NotReachable(string target) { var actual = await _sut.FindByNameAsync(target, reachable: false); Assert.That(actual.IsReachable(DefaultReachableStrategy.IsReachable), Is.False); - // TODO: Remove argument after remove obsolete method + // TODO: Remove when remove obsolete method, Already copied to DefaultReachableStrategyTest. } } diff --git a/Tests/Runtime/MonkeyTest.cs b/Tests/Runtime/MonkeyTest.cs index e8aca65..7354a88 100644 --- a/Tests/Runtime/MonkeyTest.cs +++ b/Tests/Runtime/MonkeyTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Cysharp.Threading.Tasks; @@ -17,6 +18,7 @@ using TestHelper.Random; using TestHelper.RuntimeInternals; using UnityEngine; +using UnityEngine.TestTools; // ReSharper disable MethodSupportsCancellation @@ -25,7 +27,7 @@ namespace TestHelper.Monkey [TestFixture] public class MonkeyTest { - private const string TestScene = "Packages/com.nowsprinting.test-helper.monkey/Tests/Scenes/Operators.unity"; + private const string TestScene = "../Scenes/Operators.unity"; private IEnumerable _operators; private InteractiveComponentCollector _interactiveComponentCollector; @@ -202,11 +204,11 @@ public async Task Run_withGizmos_showGizmosAndReverted() [Test] [LoadScene(TestScene)] - public void GetOperators_GotAllInteractableComponentAndOperators() + public void GetLotteryEntries_GotAllInteractableComponentAndOperators() { - var operators = Monkey.GetOperators(_interactiveComponentCollector); + var lotteryEntries = Monkey.GetLotteryEntries(_interactiveComponentCollector); var actual = new List(); - foreach (var (component, @operator) in operators) + foreach (var (component, @operator) in lotteryEntries) { actual.Add( $"{component.gameObject.name}|{component.GetType().Name}|{@operator.GetType().Name}"); @@ -231,13 +233,13 @@ public void GetOperators_GotAllInteractableComponentAndOperators() [Test] [LoadScene(TestScene)] - public void GetOperators_WithIgnoreAnnotation_Excluded() + public void GetLotteryEntries_WithIgnoreAnnotation_Excluded() { GameObject.Find("UsingOnPointerClickHandler").AddComponent(); - var operators = Monkey.GetOperators(_interactiveComponentCollector); + var lotteryEntries = Monkey.GetLotteryEntries(_interactiveComponentCollector); var actual = new List(); - foreach (var (component, _) in operators) + foreach (var (component, _) in lotteryEntries) { actual.Add($"{component.gameObject.name}"); } @@ -257,7 +259,7 @@ public void LotteryOperator_NothingOperators_ReturnNull() } [Test] - [LoadScene("Packages/com.nowsprinting.test-helper.monkey/Tests/Scenes/PhysicsRaycasterSandbox.unity")] + [LoadScene("../Scenes/PhysicsRaycasterSandbox.unity")] public void LotteryOperator_NotReachableComponentOnly_ReturnNull() { var cube = GameObject.Find("Cube"); @@ -273,7 +275,7 @@ public void LotteryOperator_NotReachableComponentOnly_ReturnNull() } [Test] - [LoadScene("Packages/com.nowsprinting.test-helper.monkey/Tests/Scenes/PhysicsRaycasterSandbox.unity")] + [LoadScene("../Scenes/PhysicsRaycasterSandbox.unity")] public void LotteryOperator_BingoReachableComponent_ReturnOperator() { var cube = GameObject.Find("Cube"); @@ -465,5 +467,79 @@ public async Task Run_withScreenshots_noInteractiveComponent_takeScreenshot() Assert.That(_path, Does.Exist); } } + + [TestFixture] + public class Verbose + { + private IEnumerable _operators; + private InteractiveComponentCollector _interactiveComponentCollector; + + [SetUp] + public void SetUp() + { + _operators = new IOperator[] + { + new UGUIClickOperator(), // click + new UGUIClickAndHoldOperator(1), // click and hold 1ms + new UGUITextInputOperator() + }; + _interactiveComponentCollector = new InteractiveComponentCollector(operators: _operators); + } + + [Test] + [LoadScene(TestScene)] + public void GetLotteryEntriesWithoutVerbose_NotOutputLog() + { + var lotteryEntries = Monkey.GetLotteryEntries(_interactiveComponentCollector); + Assume.That(lotteryEntries.Count, Is.GreaterThan(0)); + + LogAssert.NoUnexpectedReceived(); + } + + [Test] + [LoadScene(TestScene)] + public void GetLotteryEntriesWithVerbose_LogLotteryEntries() + { + GameObject.Find("UsingOnPointerClickHandler").AddComponent(); + + var spyLogger = new SpyLogger(); + var lotteryEntries = Monkey.GetLotteryEntries(_interactiveComponentCollector, verboseLogger: spyLogger); + Assume.That(lotteryEntries.Count, Is.GreaterThan(0)); + + Assert.That(spyLogger.Messages.Count, Is.EqualTo(1)); + Assert.That(spyLogger.Messages[0], Does.StartWith("Lottery entries: ")); + Assert.That(spyLogger.Messages[0], Does.Match(@"\[Ignored\]UsingOnPointerClickHandler\(\d+\)")); + Assert.That(spyLogger.Messages[0], Does.Match(@"UsingPointerClickEventTrigger\(\d+\)")); + Assert.That(spyLogger.Messages[0], Does.Match(@"UsingOnPointerDownUpHandler\(\d+\)")); + Assert.That(spyLogger.Messages[0], Does.Match(@"UsingPointerDownUpEventTrigger\(\d+\)")); + Assert.That(spyLogger.Messages[0], Does.Match(@"UsingMultipleEventTriggers\(\d+\)")); + Assert.That(spyLogger.Messages[0], Does.Match(@"DestroyItselfIfPointerDown\(\d+\)")); + Assert.That(spyLogger.Messages[0], Does.Match(@"InputField\(\d+\)")); + } + + [Test] + [LoadScene("../Scenes/PhysicsRaycasterSandbox.unity")] // no interactable objects + public void GetLotteryEntriesWithVerbose_NoInteractableObject_LogNoLotteryEntries() + { + var spyLogger = new SpyLogger(); + var lotteryEntries = Monkey.GetLotteryEntries(_interactiveComponentCollector, verboseLogger: spyLogger); + Assume.That(lotteryEntries, Is.Empty); + + Assert.That(spyLogger.Messages.Count, Is.EqualTo(1)); + Assert.That(spyLogger.Messages[0], Is.EqualTo("No lottery entries.")); + } + + [Test] + [LoadScene("../Scenes/PhysicsRaycasterSandbox.unity")] // no interactable and reachable objects + public void LotteryOperatorWithVerbose_NotReachableComponentOnly_LogNoLotteryEntries() + { + var spyLogger = new SpyLogger(); + var lotteryEntries = Monkey.GetLotteryEntries(_interactiveComponentCollector).ToList(); + var random = new RandomWrapper(); + Monkey.LotteryOperator(lotteryEntries, random, DefaultReachableStrategy.IsReachable, spyLogger); + + Assert.That(spyLogger.Messages, Does.Contain("Lottery entries are empty or all of not reachable.")); + } + } } } From 94d8eade394724c9ad517df21a21fbc1ed7e4545 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Sun, 12 May 2024 11:34:43 +0900 Subject: [PATCH 6/7] Fix for older Unity --- Runtime/Monkey.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Runtime/Monkey.cs b/Runtime/Monkey.cs index c52a094..1a53e0f 100644 --- a/Runtime/Monkey.cs +++ b/Runtime/Monkey.cs @@ -145,11 +145,19 @@ public static async UniTask RunStep( { if (component.gameObject.TryGetComponent(typeof(IgnoreAnnotation), out _)) { - dictionary?.TryAdd(component.gameObject, "Ignored"); + if (dictionary != null && !dictionary.Keys.Contains(component.gameObject)) + { + dictionary.Add(component.gameObject, "Ignored"); + } + continue; } - dictionary?.TryAdd(component.gameObject, null); + if (dictionary != null && !dictionary.Keys.Contains(component.gameObject)) + { + dictionary.Add(component.gameObject, null); + } + yield return (component, iOperator); } From 987af988768d57a3616de051e9c045a2d19745ce Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Sun, 12 May 2024 14:03:47 +0900 Subject: [PATCH 7/7] Mod README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 074514a..8957915 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Configurations in `MonkeyConfig`: - **SecondsToErrorForNoInteractiveComponent**: Seconds to determine that an error has occurred when an object that can be interacted with does not exist - **Random**: Random generator - **Logger**: Logger +- **Verbose**: Output verbose log if true - **Gizmos**: Show Gizmos on `GameView` during running monkey test if true - **Screenshots**: Take screenshots during running the monkey test if set a `ScreenshotOptions` instance. - **Directory**: Directory to save screenshots. If omitted, the directory specified by command line argument "-testHelperScreenshotDirectory" is used. If the command line argument is also omitted, `Application.persistentDataPath` + "/TestHelper/Screenshots/" is used.