using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor.IMGUI.Controls; using UnityEditor.TestTools.TestRunner.Api; using UnityEditor.TestTools.TestRunner.GUI.TestAssets; using UnityEngine; namespace UnityEditor.TestTools.TestRunner.GUI { [Serializable] internal class TestListGUI { private static readonly GUIContent s_GUIRunSelectedTests = EditorGUIUtility.TrTextContent("Run Selected", "Run selected test(s)"); private static readonly GUIContent s_GUIRunAllTests = EditorGUIUtility.TrTextContent("Run All", "Run all tests"); private static readonly GUIContent s_GUIRerunFailedTests = EditorGUIUtility.TrTextContent("Rerun Failed", "Rerun all failed tests"); private static readonly GUIContent s_GUIRun = EditorGUIUtility.TrTextContent("Run"); private static readonly GUIContent s_GUIRunUntilFailed = EditorGUIUtility.TrTextContent("Run Until Failed"); private static readonly GUIContent s_GUIRun100Times = EditorGUIUtility.TrTextContent("Run 100 times"); private static readonly GUIContent s_GUIOpenTest = EditorGUIUtility.TrTextContent("Open source code"); private static readonly GUIContent s_GUIOpenErrorLine = EditorGUIUtility.TrTextContent("Open error line"); private static readonly GUIContent s_GUIClearResults = EditorGUIUtility.TrTextContent("Clear Results", "Clear all test results"); private static readonly GUIContent s_SaveResults = EditorGUIUtility.TrTextContent("Export Results", "Save the latest test results to a file"); private static readonly GUIContent s_GUICancelRun = EditorGUIUtility.TrTextContent("Cancel Run"); [SerializeField] private TestRunnerWindow m_Window; [NonSerialized] private TestRunnerApi m_TestRunnerApi; [NonSerialized] internal TestMode m_TestMode; [NonSerialized] internal bool m_RunOnPlatform; [NonSerialized] internal bool m_buildOnly; [SerializeField] private TestRunProgress runProgress; public Dictionary filteredTree { get; set; } public List newResultList { get { return m_NewResultList; } set { m_NewResultList = value; m_ResultByKey = null; } } [SerializeField] private List m_NewResultList = new List(); private Dictionary m_ResultByKey; internal Dictionary ResultsByKey { get { if (m_ResultByKey == null) { m_ResultByKey = new Dictionary(); foreach (var result in newResultList) { if (m_ResultByKey.ContainsKey(result.uniqueId)) { Debug.LogWarning($"Multiple tests has the same unique id '{result.uniqueId}', their results will be overwritten."); continue; } m_ResultByKey.Add(result.uniqueId, result); } } return m_ResultByKey; } } [SerializeField] private string m_ResultText; [SerializeField] private string m_ResultStacktrace; private TreeViewController m_TestListTree; [SerializeField] internal TreeViewState m_TestListState; [SerializeField] internal TestRunnerUIFilter m_TestRunnerUIFilter = new TestRunnerUIFilter(); private Vector2 m_TestInfoScroll, m_TestListScroll; private List m_QueuedResults = new List(); private ITestResultAdaptor m_LatestTestResults; public TestListGUI() { MonoCecilHelper = new MonoCecilHelper(); AssetsDatabaseHelper = new AssetsDatabaseHelper(); GuiHelper = new GuiHelper(MonoCecilHelper, AssetsDatabaseHelper); TestRunnerApi.runProgressChanged.AddListener(UpdateProgressStatus); } private void UpdateProgressStatus(TestRunProgress progress) { runProgress = progress; } private IMonoCecilHelper MonoCecilHelper { get; set; } private IAssetsDatabaseHelper AssetsDatabaseHelper { get; set; } private IGuiHelper GuiHelper { get; set; } private struct PlayerMenuItem { public GUIContent name; public bool filterSelectedTestsOnly; } [SerializeField] private int m_SelectedOption; public void PrintHeadPanel() { if (m_RunOnPlatform) { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); GUILayout.Label("Running on " + EditorUserBuildSettings.activeBuildTarget); EditorGUILayout.EndHorizontal(); } using (new EditorGUI.DisabledScope(m_TestListTree == null || IsBusy())) { m_TestRunnerUIFilter.OnModeGUI(); DrawFilters(); } } public void PrintProgressBar(Rect rect) { if (runProgress == null || runProgress.HasFinished || string.IsNullOrEmpty(runProgress.RunGuid)) { return; } EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(), runProgress.Progress, runProgress.CurrentStepName); if (GUILayout.Button(s_GUICancelRun, GUILayout.Width(100))) { TestRunnerApi.CancelTestRun(runProgress.RunGuid); } EditorGUILayout.EndHorizontal(); } public void PrintBottomPanel() { using (new EditorGUI.DisabledScope(m_TestListTree == null || IsBusy())) { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); { using (new EditorGUI.DisabledScope(m_LatestTestResults == null)) { if (GUILayout.Button(s_SaveResults)) { var filePath = EditorUtility.SaveFilePanel(s_SaveResults.text, "", $"TestResults_{DateTime.Now:yyyyMMdd_HHmmss}.xml", "xml"); if (!string.IsNullOrEmpty(filePath)) { TestRunnerApi.SaveResultToFile(m_LatestTestResults, filePath); } GUIUtility.ExitGUI(); } } if (GUILayout.Button(s_GUIClearResults)) { foreach (var result in newResultList) { result.Clear(); } m_TestRunnerUIFilter.UpdateCounters(newResultList, filteredTree); Reload(); GUIUtility.ExitGUI(); } GUILayout.FlexibleSpace(); using (new EditorGUI.DisabledScope(m_TestRunnerUIFilter.FailedCount == 0)) { if (GUILayout.Button(s_GUIRerunFailedTests)) { RunTests(RunFilterType.RunFailed); GUIUtility.ExitGUI(); } } using (new EditorGUI.DisabledScope(m_TestListTree == null || !m_TestListTree.HasSelection())) { if (GUILayout.Button(s_GUIRunSelectedTests)) { RunTests(RunFilterType.RunSelected); GUIUtility.ExitGUI(); } } if (GUILayout.Button(s_GUIRunAllTests)) { RunTests(RunFilterType.RunAll); GUIUtility.ExitGUI(); } if (m_TestMode == TestMode.PlayMode && m_RunOnPlatform) { PlayerMenuItem[] menuItems; if (EditorUserBuildSettings.installInBuildFolder) { // Note: We select here m_buildOnly = false, so build location dialog won't show up // The player won't actually be ran when using together with EditorUserBuildSettings.installInBuildFolder m_buildOnly = false; menuItems = new [] { new PlayerMenuItem() { name = new GUIContent("Install All Tests In Build Folder"), filterSelectedTestsOnly = false }, new PlayerMenuItem() { name = new GUIContent("Install Selected Tests In Build Folder"), filterSelectedTestsOnly = true } }; } else { m_buildOnly = true; menuItems = new [] { new PlayerMenuItem() { name = new GUIContent($"{GetBuildText()} All Tests"), filterSelectedTestsOnly = false }, new PlayerMenuItem() { name = new GUIContent($"{GetBuildText()} Selected Tests"), filterSelectedTestsOnly = true }, }; } if (GUILayout.Button(GUIContent.none, EditorStyles.toolbarDropDown)) { Vector2 mousePos = Event.current.mousePosition; EditorUtility.DisplayCustomMenu(new Rect(mousePos.x, mousePos.y, 0, 0), menuItems.Select(m => m.name).ToArray(), -1, (object userData, string[] options, int selected) => RunTests(menuItems[selected].filterSelectedTestsOnly ? RunFilterType.BuildSelected : RunFilterType.BuildAll), menuItems); } } } EditorGUILayout.EndHorizontal(); } } private string GetBuildText() { switch (EditorUserBuildSettings.activeBuildTarget) { case BuildTarget.Android: if (EditorUserBuildSettings.exportAsGoogleAndroidProject) return "Export"; break; case BuildTarget.iOS: return "Export"; } return "Build"; } private string PickBuildLocation() { var target = EditorUserBuildSettings.activeBuildTarget; var targetGroup = BuildPipeline.GetBuildTargetGroup(target); var lastLocation = EditorUserBuildSettings.GetBuildLocation(target); var extension = PostprocessBuildPlayer.GetExtensionForBuildTarget(targetGroup, target, BuildOptions.None); var defaultName = FileUtil.GetLastPathNameComponent(lastLocation); lastLocation = string.IsNullOrEmpty(lastLocation) ? string.Empty : Path.GetDirectoryName(lastLocation); bool updateExistingBuild; var location = EditorUtility.SaveBuildPanel(target, $"{GetBuildText()} {target}", lastLocation, defaultName, extension, out updateExistingBuild); if (!string.IsNullOrEmpty(location)) EditorUserBuildSettings.SetBuildLocation(target, location); return location; } private void DrawFilters() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); m_TestRunnerUIFilter.Draw(); EditorGUILayout.EndHorizontal(); } public bool HasTreeData() { return m_TestListTree != null; } public void RenderTestList() { if (m_TestListTree == null) { GUILayout.Label("Loading..."); return; } m_TestListScroll = EditorGUILayout.BeginScrollView(m_TestListScroll, GUILayout.ExpandWidth(true), GUILayout.MaxWidth(2000)); if (m_TestListTree.data.root == null || m_TestListTree.data.rowCount == 0 || (!m_TestListTree.isSearching && !m_TestListTree.data.GetItem(0).hasChildren)) { if (m_TestRunnerUIFilter.IsFiltering) { var notMatchFoundStyle = new GUIStyle("label"); notMatchFoundStyle.normal.textColor = Color.red; notMatchFoundStyle.alignment = TextAnchor.MiddleCenter; GUILayout.Label("No match found", notMatchFoundStyle); if (GUILayout.Button("Clear filters")) { m_TestRunnerUIFilter.Clear(); UpdateTestTree(); m_Window.Repaint(); } } RenderNoTestsInfo(); } else { var treeRect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); var treeViewKeyboardControlId = GUIUtility.GetControlID(FocusType.Keyboard); m_TestListTree.OnGUI(treeRect, treeViewKeyboardControlId); } m_TestRunnerUIFilter.UpdateCounters(newResultList, filteredTree); EditorGUILayout.EndScrollView(); } private void RenderNoTestsInfo() { var testScriptAssetsCreator = new TestScriptAssetsCreator(); if (!testScriptAssetsCreator.ActiveFolderContainsTestAssemblyDefinition()) { var noTestsText = "No tests to show."; if (!PlayerSettings.playModeTestRunnerEnabled) { const string testsMustLiveInCustomTestAssemblies = "Test scripts can be added to assemblies referencing the \"nunit.framework.dll\" library " + "or folders with Assembly Definition References targeting \"UnityEngine.TestRunner\" or \"UnityEditor.TestRunner\"."; noTestsText += Environment.NewLine + testsMustLiveInCustomTestAssemblies; } EditorGUILayout.HelpBox(noTestsText, MessageType.Info); if (GUILayout.Button("Create a new Test Assembly Folder in the active path.")) { testScriptAssetsCreator.AddNewFolderWithTestAssemblyDefinition(m_TestMode == TestMode.EditMode); } } const string notTestAssembly = "Test Scripts can only be created inside test assemblies."; const string createTestScriptInCurrentFolder = "Create a new Test Script in the active path."; var canAddTestScriptAndItWillCompile = testScriptAssetsCreator.TestScriptWillCompileInActiveFolder(); using (new EditorGUI.DisabledScope(!canAddTestScriptAndItWillCompile)) { var createTestScriptInCurrentFolderGUI = !canAddTestScriptAndItWillCompile ? new GUIContent(createTestScriptInCurrentFolder, notTestAssembly) : new GUIContent(createTestScriptInCurrentFolder); if (GUILayout.Button(createTestScriptInCurrentFolderGUI)) { testScriptAssetsCreator.AddNewTestScript(); } } } public void RenderDetails(float width) { m_TestInfoScroll = EditorGUILayout.BeginScrollView(m_TestInfoScroll, GUILayout.ExpandWidth(true)); var resultTextHeight = TestRunnerWindow.Styles.info.CalcHeight(new GUIContent(m_ResultText), width); EditorGUILayout.SelectableLabel(m_ResultText, TestRunnerWindow.Styles.info, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true), GUILayout.MinHeight(resultTextHeight)); EditorGUILayout.EndScrollView(); } public void Reload() { if (m_TestListTree != null) { m_TestListTree.ReloadData(); UpdateQueuedResults(); } } public void Repaint() { if (m_TestListTree == null || m_TestListTree.data.root == null) { return; } m_TestListTree.Repaint(); if (m_TestListTree.data.rowCount == 0) m_TestListTree.SetSelection(new int[0], false); TestSelectionCallback(m_TestListState.selectedIDs.ToArray()); } public void Init(TestRunnerWindow window, ITestAdaptor rootTest) { Init(window, new[] {rootTest}); } private void Init(TestRunnerWindow window, ITestAdaptor[] rootTests) { if (m_Window == null) { m_Window = window; } if (m_TestListTree == null) { if (m_TestListState == null) { m_TestListState = new TreeViewState(); } if (m_TestListTree == null) m_TestListTree = new TreeViewController(m_Window, m_TestListState); m_TestListTree.deselectOnUnhandledMouseDown = false; m_TestListTree.selectionChangedCallback += TestSelectionCallback; m_TestListTree.itemDoubleClickedCallback += TestDoubleClickCallback; m_TestListTree.contextClickItemCallback += TestContextClickCallback; var testListTreeViewDataSource = new TestListTreeViewDataSource(m_TestListTree, this, rootTests); m_TestListTree.Init(new Rect(), testListTreeViewDataSource, new TestListTreeViewGUI(m_TestListTree), null); } m_TestRunnerUIFilter.UpdateCounters(newResultList, filteredTree); m_TestRunnerUIFilter.RebuildTestList = () => m_TestListTree.ReloadData(); m_TestRunnerUIFilter.UpdateTestTreeRoots = UpdateTestTree; m_TestRunnerUIFilter.SearchStringChanged = s => m_TestListTree.ReloadData(); m_TestRunnerUIFilter.SearchStringCleared = () => FrameSelection(); } public void UpdateResult(TestRunnerResult result) { if (!HasTreeData()) { m_QueuedResults.Add(result); return; } if (!ResultsByKey.TryGetValue(result.uniqueId, out var testRunnerResult)) { // Add missing result due to e.g. changes in code for uniqueId due to change of package version. m_NewResultList.Add(result); ResultsByKey[result.uniqueId] = result; testRunnerResult = result; } testRunnerResult.Update(result); Repaint(); m_Window.Repaint(); } public void RunFinished(ITestResultAdaptor results) { m_LatestTestResults = results; UpdateTestTree(); } private void UpdateTestTree() { var testList = this; if (m_TestRunnerApi == null) { m_TestRunnerApi= ScriptableObject.CreateInstance(); } m_TestRunnerApi.RetrieveTestList(GetExecutionSettings(), rootTest => { testList.UpdateTestTree(new[] { rootTest }); testList.Reload(); }); } public void UpdateTestTree(ITestAdaptor[] tests) { if (!HasTreeData()) { return; } (m_TestListTree.data as TestListTreeViewDataSource).UpdateRootTest(tests); m_TestListTree.ReloadData(); Repaint(); m_Window.Repaint(); } private void UpdateQueuedResults() { foreach (var testRunnerResult in m_QueuedResults) { if (ResultsByKey.TryGetValue(testRunnerResult.uniqueId, out var existingResult)) { existingResult.Update(testRunnerResult); } } m_QueuedResults.Clear(); TestSelectionCallback(m_TestListState.selectedIDs.ToArray()); m_TestRunnerUIFilter.UpdateCounters(newResultList, filteredTree); Repaint(); m_Window.Repaint(); } internal void TestSelectionCallback(int[] selected) { if (m_TestListTree != null && selected.Length > 0) { if (m_TestListTree != null) { var node = m_TestListTree.FindItem(selected[0]); if (node is TestTreeViewItem) { var test = node as TestTreeViewItem; m_ResultText = test.GetResultText(); m_ResultStacktrace = test.result.stacktrace; } } } else if (selected.Length == 0) { m_ResultText = ""; } } private void TestDoubleClickCallback(int id) { if (IsBusy()) return; RunTests(RunFilterType.RunSpecific, id); GUIUtility.ExitGUI(); } private void RunTests(RunFilterType runFilter, params int[] specificTests) { if (EditorUtility.scriptCompilationFailed) { Debug.LogError("Fix compilation issues before running tests"); return; } var filters = ConstructFilter(runFilter, specificTests); if (filters == null) { return; } foreach (var filter in filters) { filter.ClearResults(newResultList.OfType().ToDictionary(result => result.Id)); } var testFilters = filters.Select(filter => new Filter { testMode = m_TestMode, assemblyNames = filter.assemblyNames, categoryNames = filter.categoryNames, groupNames = filter.groupNames, testNames = filter.testNames, }).ToArray(); #if UNITY_2022_2_OR_NEWER var executionSettings = CreateExecutionSettings(m_RunOnPlatform ? EditorUserBuildSettings.activeBuildTarget : null, testFilters); #else var executionSettings = CreateExecutionSettings(m_RunOnPlatform ? EditorUserBuildSettings.activeBuildTarget : (BuildTarget?)null, testFilters); #endif if (runFilter == RunFilterType.BuildAll || runFilter == RunFilterType.BuildSelected) { var runSettings = new PlayerLauncherTestRunSettings(); runSettings.buildOnly = m_buildOnly; if (runSettings.buildOnly) { runSettings.buildOnlyLocationPath = PickBuildLocation(); if (string.IsNullOrEmpty(runSettings.buildOnlyLocationPath)) { Debug.LogWarning("Aborting, build selection was canceled."); return; } } executionSettings.overloadTestRunSettings = runSettings; Debug.Log(executionSettings.ToString()); } if (m_TestRunnerApi == null) { m_TestRunnerApi = ScriptableObject.CreateInstance(); } m_TestRunnerApi.Execute(executionSettings); if (executionSettings.targetPlatform != null && !(runFilter == RunFilterType.BuildAll || runFilter == RunFilterType.BuildSelected)) { GUIUtility.ExitGUI(); } } private void TestContextClickCallback(int id) { if (id == 0) return; var m = new GenericMenu(); var multilineSelection = m_TestListState.selectedIDs.Count > 1; if (!multilineSelection) { var testNode = GetSelectedTest(); var isNotSuite = !testNode.IsGroupNode; if (isNotSuite) { if (!string.IsNullOrEmpty(m_ResultStacktrace)) { m.AddItem(s_GUIOpenErrorLine, false, data => { if (!GuiHelper.OpenScriptInExternalEditor(m_ResultStacktrace)) { GuiHelper.OpenScriptInExternalEditor(testNode.type, testNode.method); } }, ""); } m.AddItem(s_GUIOpenTest, false, data => GuiHelper.OpenScriptInExternalEditor(testNode.type, testNode.method), ""); m.AddSeparator(""); } } if (!IsBusy()) { m.AddItem(multilineSelection ? s_GUIRunSelectedTests : s_GUIRun, false, data => RunTests(RunFilterType.RunSelected), ""); } else m.AddDisabledItem(multilineSelection ? s_GUIRunSelectedTests : s_GUIRun, false); m.ShowAsContext(); } private enum RunFilterType { RunAll, RunSelected, RunFailed, RunSpecific, BuildAll, BuildSelected } private struct FilterConstructionStep { public int Id; public TreeViewItem Item; } private UITestRunnerFilter[] ConstructFilter(RunFilterType runFilter, int[] specificTests = null) { if ((runFilter == RunFilterType.RunAll || runFilter == RunFilterType.BuildAll) && !m_TestRunnerUIFilter.IsFiltering) { // Shortcut for RunAll, which will not trigger any explicit tests return new[] {new UITestRunnerFilter()}; } var includedIds = GetIdsIncludedInRunFilter(runFilter, specificTests); var testsInFilter = includedIds.Select(id => m_TestListTree.FindItem(id)).Cast() .SelectMany(item => item.GetMinimizedSelectedTree()).Distinct().ToArray(); if (testsInFilter.Length == 0) { return null; } if (testsInFilter.Any(test => test.Parent == null)) { // The root element is included in the minified list, which means we are running all tests // It should however trigger explicit tests, which is done by a groupNames filter matching all groups return new[] { new UITestRunnerFilter() { groupNames = new[] {".*"} } }; } var assemblies = testsInFilter.Where(test => test.IsTestAssembly).ToArray(); var tests = testsInFilter.Where(test => !test.IsTestAssembly).ToArray(); var filters = new List(); if (tests.Length > 0) { filters.Add(new UITestRunnerFilter { testNames = tests.Select(test => test.FullName).ToArray() }); } if (assemblies.Length > 0) { filters.Add(new UITestRunnerFilter { assemblyNames = assemblies.Select(test => { // remove .dll from the end of the name var name = test.Name; if (name.EndsWith(".dll")) name = name.Substring(0, name.Length - 4); return name; }).ToArray() }); } return filters.ToArray(); } private IEnumerable GetIdsIncludedInRunFilter(RunFilterType runFilter, int[] specificTests) { switch (runFilter) { case RunFilterType.RunSelected: case RunFilterType.BuildSelected: return m_TestListState.selectedIDs; case RunFilterType.RunSpecific: if (specificTests == null) { throw new ArgumentNullException( $"For {nameof(RunFilterType.RunSpecific)}, the {nameof(specificTests)} argument must not be null."); } return specificTests; default: return m_TestListTree.GetRowIDs(); } } private TestTreeViewItem GetSelectedTest() { foreach (var lineId in m_TestListState.selectedIDs) { var line = m_TestListTree.FindItem(lineId); if (line is TestTreeViewItem) { return line as TestTreeViewItem; } } return null; } private void FrameSelection() { if (m_TestListTree.HasSelection()) { var firstClickedID = m_TestListState.selectedIDs.First() == m_TestListState.lastClickedID ? m_TestListState.selectedIDs.Last() : m_TestListState.selectedIDs.First(); m_TestListTree.Frame(firstClickedID, true, false); } } public void RebuildUIFilter() { m_TestRunnerUIFilter.UpdateCounters(newResultList, filteredTree); if (m_TestRunnerUIFilter.IsFiltering) { m_TestListTree.ReloadData(); } } private static bool IsBusy() { return TestRunnerApi.IsRunActive() || EditorApplication.isCompiling || EditorApplication.isPlaying; } public ExecutionSettings GetExecutionSettings() { var filter = new Filter { testMode = m_TestMode }; #if UNITY_2022_2_OR_NEWER return CreateExecutionSettings(m_RunOnPlatform ? EditorUserBuildSettings.activeBuildTarget : null, filter); #else return CreateExecutionSettings(m_RunOnPlatform ? EditorUserBuildSettings.activeBuildTarget : (BuildTarget?)null, filter); #endif } private static ExecutionSettings CreateExecutionSettings(BuildTarget? buildTarget, params Filter[] filters) { return new ExecutionSettings(filters) { targetPlatform = buildTarget, }; } } }