Initial project commit

This commit is contained in:
2026-01-08 16:50:20 +00:00
commit f0c5a8b267
29596 changed files with 4861782 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal class AssetsDatabaseHelper : IAssetsDatabaseHelper
{
public void OpenAssetInItsDefaultExternalEditor(string assetPath, int line)
{
var asset = AssetDatabase.LoadMainAssetAtPath(assetPath);
AssetDatabase.OpenAsset(asset, line);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 740b3785866edda4b8d1e1a05570a5f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e88eef57957d4440c8a7ff2ef9dd3d97
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// Provides methods for dealing with common bit operations.
/// </summary>
internal static class BitUtility
{
/// <summary>
/// Evaluates the cardinality of an integer, treating the value as a bit set.
/// Optimization based on http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel.
/// </summary>
/// <param name="integer">The input integer value.</param>
/// <returns>The number of bits set in the provided input integer value.</returns>
internal static int GetCardinality(int integer)
{
unchecked
{
integer = integer - ((integer >> 1) & 0x55555555);
integer = (integer & 0x33333333) + ((integer >> 2) & 0x33333333);
integer = (((integer + (integer >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24;
}
return integer;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 24a5f331fec74c9aa9e1e5d74b5a9589
timeCreated: 1601747849

View File

@@ -0,0 +1,118 @@
using System;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// A flag enum content provider to be used with the <see cref="SelectionDropDown" /> control.
/// </summary>
/// <typeparam name="T">The flag enum type.</typeparam>
internal class FlagEnumContentProvider<T> : ISelectionDropDownContentProvider where T : Enum
{
private readonly Action<T> m_ValueChangedCallback;
private readonly T[] m_Values;
internal Func<string, string> DisplayNameGenerator = ObjectNames.NicifyVariableName;
private T m_CurrentValue;
/// <summary>
/// Creates a new instance of the <see cref="FlagEnumContentProvider{T}" /> class.
/// </summary>
/// <param name="initialValue">The initial selection value.</param>
/// <param name="valueChangedCallback">The callback to be invoked on selection change.</param>
/// <exception cref="ArgumentException">
/// Thrown if the generic enum parameter type is not integer based
/// or if the initial selection value is empty.
/// </exception>
/// <exception cref="ArgumentNullException">Thrown if the provided change callback is null.</exception>
public FlagEnumContentProvider(T initialValue, Action<T> valueChangedCallback)
{
if (Enum.GetUnderlyingType(typeof(T)) != typeof(int))
{
throw new ArgumentException("Argument underlying type must be integer.");
}
if ((int)(object)initialValue == 0)
{
throw new ArgumentException("The initial value must not be an empty set.", nameof(initialValue));
}
if (valueChangedCallback == null)
{
throw new ArgumentNullException(nameof(valueChangedCallback), "The value change callback must not be null.");
}
m_CurrentValue = initialValue;
m_Values = (T[])Enum.GetValues(typeof(T));
m_ValueChangedCallback = valueChangedCallback;
}
public int Count => m_Values.Length;
public bool IsMultiSelection => true;
public string GetName(int index)
{
return ValidateIndexBounds(index) ? DisplayNameGenerator(m_Values[index].ToString()) : string.Empty;
}
public int[] SeparatorIndices => new int[0];
public bool IsSelected(int index)
{
return ValidateIndexBounds(index) && IsSet(m_Values[index]);
}
public void SelectItem(int index)
{
if (!ValidateIndexBounds(index))
{
return;
}
if (ChangeValue(m_Values[index]))
{
m_ValueChangedCallback(m_CurrentValue);
}
}
private bool ChangeValue(T flag)
{
var value = flag;
var count = GetSetCount();
if (IsSet(value))
{
if (count == 1)
{
return false;
}
m_CurrentValue = FlagEnumUtility.RemoveFlag(m_CurrentValue, flag);
return true;
}
m_CurrentValue = FlagEnumUtility.SetFlag(m_CurrentValue, flag);
return true;
}
private bool IsSet(T flag)
{
return FlagEnumUtility.HasFlag(m_CurrentValue, flag);
}
private int GetSetCount()
{
return BitUtility.GetCardinality((int)(object)m_CurrentValue);
}
private bool ValidateIndexBounds(int index)
{
if (index < 0 || index >= Count)
{
Debug.LogError($"Requesting item index {index} from a collection of size {Count}");
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d31403ec6b334194bdeb4c5ebad64097
timeCreated: 1600072403

View File

@@ -0,0 +1,74 @@
using System;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// Provides methods for dealing with common enumerator operations.
/// </summary>
internal static class FlagEnumUtility
{
/// <summary>
/// Checks for the presence of a flag in a flag enum value.
/// </summary>
/// <param name="value">The value to check for the presence of the flag.</param>
/// <param name="flag">The flag whose presence is to be checked.</param>
/// <typeparam name="T">The flag enum type.</typeparam>
/// <returns></returns>
internal static bool HasFlag<T>(T value, T flag) where T : Enum
{
ValidateUnderlyingType<T>();
var intValue = (int)(object)value;
var intFlag = (int)(object)flag;
return (intValue & intFlag) == intFlag;
}
/// <summary>
/// Sets a flag in a flag enum value.
/// </summary>
/// <param name="value">The value where the flag should be set.</param>
/// <param name="flag">The flag to be set.</param>
/// <typeparam name="T">The flag enum type.</typeparam>
/// <returns>The input value with the flag set.</returns>
internal static T SetFlag<T>(T value, T flag) where T : Enum
{
ValidateUnderlyingType<T>();
var intValue = (int)(object)value;
var intFlag = (int)(object)flag;
var result = intValue | intFlag;
return (T)Enum.ToObject(typeof(T), result);
}
/// <summary>
/// Removes a flag in a flag enum value.
/// </summary>
/// <param name="value">The value where the flag should be removed.</param>
/// <param name="flag">The flag to be removed.</param>
/// <typeparam name="T">The flag enum type.</typeparam>
/// <returns>The input value with the flag removed.</returns>
internal static T RemoveFlag<T>(T value, T flag) where T : Enum
{
ValidateUnderlyingType<T>();
var intValue = (int)(object)value;
var intFlag = (int)(object)flag;
var result = intValue & ~intFlag;
return (T)Enum.ToObject(typeof(T), result);
}
/// <summary>
/// Validates that the underlying type of an enum is integer.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <exception cref="ArgumentException">Thrown if the underlying type of the enum type parameter is not integer.</exception>
private static void ValidateUnderlyingType<T>() where T : Enum
{
if (Enum.GetUnderlyingType(typeof(T)) != typeof(int))
{
throw new ArgumentException("Argument underlying type must be integer.");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea6cc55375344f10834cf6fa65197525
timeCreated: 1600072466

View File

@@ -0,0 +1,101 @@
using System;
using System.Linq;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// A generic type content provider to be used with the <see cref="SelectionDropDown" /> control.
/// </summary>
/// <typeparam name="T">The type of values represented by content elements.</typeparam>
internal class GenericItemContentProvider<T> : ISelectionDropDownContentProvider where T : IEquatable<T>
{
private readonly ISelectableItem<T>[] m_Items;
private readonly Action<T> m_ValueChangedCallback;
private T m_CurrentValue;
/// <summary>
/// Creates a new instance of the <see cref="GenericItemContentProvider{T}" /> class.
/// </summary>
/// <param name="initialValue">The initial selection value.</param>
/// <param name="items">The set of selectable items.</param>
/// <param name="separatorIndices">The indices of items which should be followed by separator lines.</param>
/// <param name="valueChangedCallback"></param>
/// <exception cref="ArgumentNullException">Thrown if any of the provided arguments is null, except for the separator indices.</exception>
/// <exception cref="ArgumentException">Thrown if the set of items is empty or does not contain the initial selection value.</exception>
public GenericItemContentProvider(T initialValue, ISelectableItem<T>[] items, int[] separatorIndices, Action<T> valueChangedCallback)
{
if (initialValue == null)
{
throw new ArgumentNullException(nameof(initialValue), "The initial selection value must not be null.");
}
if (items == null)
{
throw new ArgumentNullException(nameof(items), "The set of items must not be null.");
}
if (valueChangedCallback == null)
{
throw new ArgumentNullException(nameof(valueChangedCallback), "The value change callback must not be null.");
}
if (items.Length == 0)
{
throw new ArgumentException("The set of items must not be empty.", nameof(items));
}
if (!items.Any(i => i.Value.Equals(initialValue)))
{
throw new ArgumentException("The initial selection value must be in the items set.", nameof(items));
}
m_CurrentValue = initialValue;
m_Items = items;
SeparatorIndices = separatorIndices ?? new int[0];
m_ValueChangedCallback = valueChangedCallback;
}
public int Count => m_Items.Length;
public bool IsMultiSelection => false;
public string GetName(int index)
{
return ValidateIndexBounds(index) ? m_Items[index].DisplayName : string.Empty;
}
public int[] SeparatorIndices { get; }
public void SelectItem(int index)
{
if (!ValidateIndexBounds(index))
{
return;
}
if (IsSelected(index))
{
return;
}
m_CurrentValue = m_Items[index].Value;
m_ValueChangedCallback(m_CurrentValue);
}
public bool IsSelected(int index)
{
return ValidateIndexBounds(index) && m_Items[index].Value.Equals(m_CurrentValue);
}
private bool ValidateIndexBounds(int index)
{
if (index < 0 || index >= Count)
{
Debug.LogError($"Requesting item index {index} from a collection of size {Count}");
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7348ecf02a70497d9a09bd05ad2038ac
timeCreated: 1600072422

View File

@@ -0,0 +1,20 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// Defines a content element which can be used with the <see cref="GenericItemContentProvider{T}" /> content provider.
/// </summary>
internal interface ISelectableItem<out T>
{
/// <summary>
/// The value represented by this item.
/// </summary>
T Value { get; }
/// <summary>
/// The name to be used when displaying this item.
/// </summary>
string DisplayName { get; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7b076e8f3150431baf926fe9ee030b1e
timeCreated: 1600072411

View File

@@ -0,0 +1,46 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// Defines a content provider that can be used with the <see cref="SelectionDropDown" /> control.
/// </summary>
internal interface ISelectionDropDownContentProvider
{
/// <summary>
/// The total number of items to display.
/// </summary>
int Count { get; }
/// <summary>
/// Multiple selection support.
/// Multiple selection dropdowns don't get closed on selection change.
/// </summary>
bool IsMultiSelection { get; }
/// <summary>
/// The indices of items which should be followed by separator lines.
/// </summary>
int[] SeparatorIndices { get; }
/// <summary>
/// Returns the display name of the item at the specified index.
/// </summary>
/// <param name="index">The index of the item whose display name is to be returned.</param>
/// <returns>The display name of the item at the specified index.</returns>
string GetName(int index);
/// <summary>
/// Signals a request to select the item at the specified index.
/// </summary>
/// <param name="index">The index of the item to be selected.</param>
void SelectItem(int index);
/// <summary>
/// Returns the selection status of the item at the specified index.
/// </summary>
/// <param name="index">The index of the item whose selection status is to be returned.</param>
/// <returns><c>true</c> if the item is currently selected; otherwise, <c>false</c>. </returns>
bool IsSelected(int index);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c2247a4dfeae4607949780b219a7d3c8
timeCreated: 1600072397

View File

@@ -0,0 +1,82 @@
using System;
using System.Linq;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
internal class MultiValueContentProvider<T> : ISelectionDropDownContentProvider where T : IEquatable<T>
{
private T[] m_Values;
private bool[] m_Selected;
private Action<T[]> m_SelectionChangeCallback;
public MultiValueContentProvider(T[] values, T[] selectedValues, Action<T[]> selectionChangeCallback)
{
m_Values = values ?? throw new ArgumentNullException(nameof(values));
if (selectedValues == null)
{
m_Selected = new bool[values.Length];
}
else
{
m_Selected = values.Select(value => selectedValues.Contains(value)).ToArray();
}
m_SelectionChangeCallback = selectionChangeCallback;
}
public int Count
{
get { return m_Values.Length; }
}
public bool IsMultiSelection
{
get { return true; }
}
public int[] SeparatorIndices
{
get { return new int[0]; }
}
public string GetName(int index)
{
if (!ValidateIndexBounds(index))
{
return string.Empty;
}
return m_Values[index].ToString();
}
public void SelectItem(int index)
{
if (!ValidateIndexBounds(index))
{
return;
}
m_Selected[index] = !m_Selected[index];
m_SelectionChangeCallback.Invoke(m_Values.Where((v, i) => m_Selected[i]).ToArray());
}
public bool IsSelected(int index)
{
if (!ValidateIndexBounds(index))
{
return false;
}
return m_Selected[index];
}
private bool ValidateIndexBounds(int index)
{
if (index < 0 || index >= Count)
{
Debug.LogError($"Requesting item index {index} from a collection of size {Count}");
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb736ed47b6da4b499431bf9c6de5890
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// A default implementation of the <see cref="ISelectableItem{T}" /> interface.
/// </summary>
/// <typeparam name="T">The type of the value represented by this content element.</typeparam>
internal class SelectableItemContent<T> : ISelectableItem<T>
{
private readonly string m_DisplayName;
/// <summary>
/// Creates a new instance of the <see cref="SelectableItemContent{T}" /> class
/// </summary>
/// <param name="itemValue">The value represented by this item.</param>
/// <param name="displayName">The display name of this item.</param>
public SelectableItemContent(T itemValue, string displayName)
{
Value = itemValue;
m_DisplayName = displayName;
}
public T Value { get; }
public string DisplayName => m_DisplayName ?? string.Empty;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f7d5e5417ccd4aa0bfa39228c674f142
timeCreated: 1600072417

View File

@@ -0,0 +1,167 @@
using System;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI.Controls
{
/// <summary>
/// A DropDown editor control accepting <see cref="ISelectionDropDownContentProvider" />-based content providers.
/// </summary>
internal class SelectionDropDown : PopupWindowContent
{
private static readonly int k_ControlId = typeof(SelectionDropDown).GetHashCode();
private readonly ISelectionDropDownContentProvider m_ContentProvider;
private readonly Vector2 m_ContentSize;
private Vector2 m_ScrollPosition = Vector2.zero;
/// <summary>
/// Creates a new instance of the <see cref="SelectionDropDown" /> editor control.
/// </summary>
/// <param name="contentProvider">The content provider to use.</param>
public SelectionDropDown(ISelectionDropDownContentProvider contentProvider)
{
m_ContentProvider = contentProvider;
var width = CalculateContentWidth();
var height = CalculateContentHeight();
m_ContentSize = new Vector2(width, height);
}
public override void OnOpen()
{
base.OnOpen();
editorWindow.wantsMouseMove = true;
editorWindow.wantsMouseEnterLeaveWindow = true;
}
public override void OnClose()
{
GUIUtility.hotControl = 0;
base.OnClose();
}
public override Vector2 GetWindowSize()
{
return m_ContentSize;
}
public override void OnGUI(Rect rect)
{
var evt = Event.current;
var contentRect = new Rect(Styles.TopMargin, 0, 1, m_ContentSize.y);
m_ScrollPosition = UnityEngine.GUI.BeginScrollView(rect, m_ScrollPosition, contentRect);
{
var yPos = Styles.TopMargin;
for (var i = 0; i < m_ContentProvider.Count; ++i)
{
var itemRect = new Rect(0, yPos, rect.width, Styles.LineHeight);
var separatorOffset = 0f;
switch (evt.type)
{
case EventType.Repaint:
var content = new GUIContent(m_ContentProvider.GetName(i));
var hover = itemRect.Contains(evt.mousePosition);
var on = m_ContentProvider.IsSelected(i);
Styles.MenuItem.Draw(itemRect, content, hover, false, on, false);
separatorOffset = DrawSeparator(i, itemRect);
break;
case EventType.MouseDown:
if (evt.button == 0 && itemRect.Contains(evt.mousePosition))
{
m_ContentProvider.SelectItem(i);
if (!m_ContentProvider.IsMultiSelection)
{
editorWindow.Close();
}
evt.Use();
}
break;
case EventType.MouseEnterWindow:
GUIUtility.hotControl = k_ControlId;
evt.Use();
break;
case EventType.MouseLeaveWindow:
GUIUtility.hotControl = 0;
evt.Use();
break;
case EventType.MouseUp:
case EventType.MouseMove:
evt.Use();
break;
}
yPos += Styles.LineHeight + separatorOffset;
}
}
UnityEngine.GUI.EndScrollView();
}
private float CalculateContentWidth()
{
var maxItemWidth = 0f;
for (var i = 0; i < m_ContentProvider.Count; ++i)
{
var itemContent = new GUIContent(m_ContentProvider.GetName(i));
var itemWidth = Styles.MenuItem.CalcSize(itemContent).x;
maxItemWidth = Mathf.Max(itemWidth, maxItemWidth);
}
return maxItemWidth;
}
private float CalculateContentHeight()
{
return m_ContentProvider.Count * Styles.LineHeight
+ m_ContentProvider.SeparatorIndices.Length * Styles.SeparatorHeight
+ Styles.TopMargin + Styles.BottomMargin;
}
private float DrawSeparator(int i, Rect itemRect)
{
if (Array.IndexOf(m_ContentProvider.SeparatorIndices, i) < 0)
{
return 0f;
}
var separatorRect = GetSeparatorRect(itemRect);
DrawRect(separatorRect, Styles.SeparatorColor);
return Styles.SeparatorHeight;
}
private static Rect GetSeparatorRect(Rect itemRect)
{
var x = itemRect.x + Styles.SeparatorMargin;
var y = itemRect.y + itemRect.height + Styles.SeparatorHeight * 0.15f;
var width = itemRect.width - 2 * Styles.SeparatorMargin;
const float height = 1f;
return new Rect(x, y, width, height);
}
private static void DrawRect(Rect rect, Color color)
{
var originalColor = UnityEngine.GUI.color;
UnityEngine.GUI.color *= color;
UnityEngine.GUI.DrawTexture(rect, EditorGUIUtility.whiteTexture);
UnityEngine.GUI.color = originalColor;
}
private static class Styles
{
public const float LineHeight = EditorGUI.kSingleLineHeight;
public const float TopMargin = 3f;
public const float BottomMargin = 1f;
public const float SeparatorHeight = 4f;
public const float SeparatorMargin = 3f;
public static readonly GUIStyle MenuItem = "MenuItem";
public static readonly Color SeparatorColor = EditorGUIUtility.isProSkin
? new Color(0.32f, 0.32f, 0.32f, 1.333f)
: new Color(0.6f, 0.6f, 0.6f, 1.333f);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f8f9b43d4e034c2d997f4eb6a0d85a96
timeCreated: 1600072477

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Unity.CodeEditor;
using UnityEditor.Utils;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal class GuiHelper : IGuiHelper
{
public GuiHelper(IMonoCecilHelper monoCecilHelper, IAssetsDatabaseHelper assetsDatabaseHelper)
{
MonoCecilHelper = monoCecilHelper;
AssetsDatabaseHelper = assetsDatabaseHelper;
GetCSFiles = (dirPath, fileExtension) =>
{
return Directory.GetFiles(dirPath, $"*{fileExtension}", SearchOption.AllDirectories)
.Select(Paths.UnifyDirectorySeparator);
};
}
internal Func<string, string, IEnumerable<string>> GetCSFiles;
protected IMonoCecilHelper MonoCecilHelper { get; private set; }
public IAssetsDatabaseHelper AssetsDatabaseHelper { get; private set; }
public IExternalCodeEditor Editor { get; internal set; }
private const string FileExtension = ".cs";
public void OpenScriptInExternalEditor(Type type, MethodInfo method)
{
var fileOpenInfo = GetFileOpenInfo(type, method);
if (string.IsNullOrEmpty(fileOpenInfo.FilePath))
{
Debug.LogWarning("Failed to open test method source code in external editor. Inconsistent filename and yield return operator in target method.");
return;
}
if (fileOpenInfo.LineNumber == 1)
{
Debug.LogWarning("Failed to get a line number for unity test method. So please find it in opened file in external editor.");
}
if (!fileOpenInfo.FilePath.Contains("Assets"))
{
(Editor ?? CodeEditor.CurrentEditor).OpenProject(fileOpenInfo.FilePath, fileOpenInfo.LineNumber, 1);
}
else
{
AssetsDatabaseHelper.OpenAssetInItsDefaultExternalEditor(fileOpenInfo.FilePath, fileOpenInfo.LineNumber);
}
}
public IFileOpenInfo GetFileOpenInfo(Type type, MethodInfo method)
{
var fileOpenInfo = MonoCecilHelper.TryGetCecilFileOpenInfo(type, method);
if (string.IsNullOrEmpty(fileOpenInfo.FilePath))
{
var dirPath = Paths.UnifyDirectorySeparator(Application.dataPath);
var allCsFiles = GetCSFiles(dirPath, FileExtension);
var fileName = allCsFiles.FirstOrDefault(x =>
x.Split(Path.DirectorySeparatorChar).Last().Equals(string.Concat(GetTestFileName(type), FileExtension)));
fileOpenInfo.FilePath = fileName ?? string.Empty;
}
if (!fileOpenInfo.FilePath.Contains("Assets"))
{
return fileOpenInfo;
}
fileOpenInfo.FilePath = FilePathToAssetsRelativeAndUnified(fileOpenInfo.FilePath);
return fileOpenInfo;
}
internal static string GetTestFileName(Type type)
{
//This handles the case of a test in a nested class, getting the name of the base class
if (type.FullName != null && type.Namespace != null && type.FullName.Contains("+"))
{
var removedNamespace = type.FullName.Substring(type.Namespace.Length + 1);
return removedNamespace.Substring(0, removedNamespace.IndexOf("+", StringComparison.Ordinal));
}
return type.Name;
}
public string FilePathToAssetsRelativeAndUnified(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return string.Empty;
#if UNITY_2021_3_OR_NEWER
return Path.GetRelativePath(Directory.GetCurrentDirectory(), filePath);
#else
filePath = Paths.UnifyDirectorySeparator(filePath);
var length = Paths.UnifyDirectorySeparator(Application.dataPath).Length - "Assets".Length;
return filePath.Substring(length);
#endif
}
public bool OpenScriptInExternalEditor(string stacktrace)
{
if (string.IsNullOrEmpty(stacktrace))
return false;
var regex = new Regex("in (?<path>.*):{1}(?<line>[0-9]+)");
var matchingLines = stacktrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).Where(x => regex.IsMatch(x)).ToList();
if (!matchingLines.Any())
return false;
var fileOpenInfos = matchingLines
.Select(x => regex.Match(x))
.Select(x =>
new FileOpenInfo
{
FilePath = x.Groups["path"].Value,
LineNumber = int.Parse(x.Groups["line"].Value)
}).ToList();
var fileOpenInfo = fileOpenInfos
.FirstOrDefault(openInfo => !string.IsNullOrEmpty(openInfo.FilePath) && File.Exists(openInfo.FilePath));
if (fileOpenInfo == null)
{
return false;
}
var filePath = FilePathToAssetsRelativeAndUnified(fileOpenInfo.FilePath);
AssetsDatabaseHelper.OpenAssetInItsDefaultExternalEditor(filePath, fileOpenInfo.LineNumber);
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0138170d24533e47b8e6c250c6d7fbc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal interface IAssetsDatabaseHelper
{
void OpenAssetInItsDefaultExternalEditor(string assetPath, int line);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 208e46d59ff6e304db0318377d20f5a1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using System;
using System.Reflection;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal interface IGuiHelper
{
bool OpenScriptInExternalEditor(string stacktrace);
void OpenScriptInExternalEditor(Type type, MethodInfo method);
IFileOpenInfo GetFileOpenInfo(Type type, MethodInfo method);
string FilePathToAssetsRelativeAndUnified(string filePath);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd57cf917f61bbb42b8f030436426ddd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 92e3104769da143888a712cdea27d950
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <inheritdoc />
internal class ActiveFolderTemplateAssetCreator : IActiveFolderTemplateAssetCreator
{
/// <inheritdoc />
public string GetActiveFolderPath()
{
return AssetDatabase.GetAssetPath(Selection.activeObject);
}
/// <inheritdoc />
public void CreateFolderWithTemplates(string defaultName, params string[] templateNames)
{
ProjectWindowUtil.CreateFolderWithTemplates(defaultName, templateNames);
}
/// <inheritdoc />
public void CreateScriptAssetFromTemplateFile(string defaultName, string templatePath)
{
ProjectWindowUtil.CreateScriptAssetFromTemplateFile(templatePath, defaultName);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f47c66e80010e4020b6803b930eb432c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
using System;
using System.IO;
using System.Linq;
using UnityEditor.Scripting.ScriptCompilation;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <inheritdoc />
internal class CustomScriptAssemblyMappingFinder : ICustomScriptAssemblyMappingFinder
{
/// <inheritdoc />
/// <exception cref="ArgumentNullException">The provided <paramref name="folderPath" /> string argument is null.</exception>
public ICustomScriptAssembly FindCustomScriptAssemblyFromFolderPath(string folderPath)
{
if (folderPath == null)
{
throw new ArgumentNullException(nameof(folderPath));
}
var scriptInFolderPath = Path.Combine(folderPath, "Foo.cs");
var customScriptAssembly = FindCustomScriptAssemblyFromScriptPath(scriptInFolderPath);
return customScriptAssembly;
}
/// <summary>
/// Finds the Custom Script Assembly associated with the provided script asset path.
/// </summary>
/// <param name="scriptPath">The script path to check.</param>
/// <returns>The associated <see cref="ICustomScriptAssembly" />; null if none.</returns>
private static ICustomScriptAssembly FindCustomScriptAssemblyFromScriptPath(string scriptPath)
{
try
{
var customScriptAssembly = EditorCompilationInterface.Instance.FindCustomScriptAssemblyFromScriptPath(scriptPath);
return new CustomScriptAssemblyWrapper(customScriptAssembly);
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// Custom Script Assembly wrapper.
/// </summary>
internal class CustomScriptAssemblyWrapper : ICustomScriptAssembly
{
internal readonly CustomScriptAssembly targetAssembly;
/// <summary>
/// Creates a new instance of the <see cref="CustomScriptAssemblyWrapper" /> class.
/// </summary>
/// <param name="assembly">The <see cref="CustomScriptAssembly" /> to be represented by the wrapper.</param>
/// <exception cref="ArgumentNullException">The provided <paramref name="assembly" /> argument is null.</exception>
internal CustomScriptAssemblyWrapper(CustomScriptAssembly assembly)
{
targetAssembly = assembly
?? throw new ArgumentNullException(nameof(assembly), "The provided assembly must not be null.");
}
/// <inheritdoc />
public bool HasPrecompiledReference(string libraryFilename)
{
var precompiledReferences = targetAssembly.PrecompiledReferences;
var libraryReferenceExists = precompiledReferences != null
&& precompiledReferences.Any(r => Path.GetFileName(r) == libraryFilename);
return libraryReferenceExists;
}
/// <inheritdoc />
public bool HasAssemblyFlag(AssemblyFlags flag)
{
var hasAssemblyFlag = (targetAssembly.AssemblyFlags & flag) == flag;
return hasAssemblyFlag;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f03c073fcc564ab582ac38999beb4b6d
timeCreated: 1603203112

View File

@@ -0,0 +1,93 @@
using System;
using System.Linq;
using UnityEditor.Scripting.ScriptCompilation;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <inheritdoc />
internal class FolderPathTestCompilationContextProvider : IFolderPathTestCompilationContextProvider
{
internal const string nUnitLibraryFilename = "nunit.framework.dll";
private static ICustomScriptAssemblyMappingFinder s_CustomScriptAssemblyMappingFinder;
internal static ICustomScriptAssemblyMappingFinder CustomScriptAssemblyMappingFinder
{
private get => s_CustomScriptAssemblyMappingFinder ?? (s_CustomScriptAssemblyMappingFinder = new CustomScriptAssemblyMappingFinder());
set => s_CustomScriptAssemblyMappingFinder = value;
}
/// <summary>
/// Checks if the provided folder path belongs to a Custom Test Assembly.
/// A Custom Test Assembly is defined by a valid reference to the precompiled NUnit library.
/// </summary>
/// <param name="folderPath">The folder path to check.</param>
/// <returns>True if a custom test assembly associated with the provided folder can be found; false otherwise.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="folderPath" /> string argument is null.</exception>
public bool FolderPathBelongsToCustomTestAssembly(string folderPath)
{
if (folderPath == null)
{
throw new ArgumentNullException(nameof(folderPath));
}
var customScriptAssembly = CustomScriptAssemblyMappingFinder.FindCustomScriptAssemblyFromFolderPath(folderPath);
var assemblyIsCustomTestAssembly = customScriptAssembly != null
&& customScriptAssembly.HasPrecompiledReference(nUnitLibraryFilename);
return assemblyIsCustomTestAssembly;
}
/// <summary>
/// Checks if the provided folder path belongs to an assembly capable of compiling Test Scripts.
/// Unless the <see cref="PlayerSettings.playModeTestRunnerEnabled" /> setting is enabled,
/// a Test Script can only be compiled in a Custom Test Assembly
/// or an (implicit or explicit) <see cref="AssemblyFlags.EditorOnly" /> assembly.
/// </summary>
/// <param name="folderPath">The folder path to check.</param>
/// <returns>True if Test Scripts can be successfully compiled when added to this folder path; false otherwise.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="folderPath" /> string argument is null.</exception>
public bool TestScriptWillCompileInFolderPath(string folderPath)
{
if (folderPath == null)
{
throw new ArgumentNullException(nameof(folderPath));
}
if (PlayerSettings.playModeTestRunnerEnabled)
{
return true;
}
var customScriptAssembly = CustomScriptAssemblyMappingFinder.FindCustomScriptAssemblyFromFolderPath(folderPath);
if (customScriptAssembly != null)
{
var assemblyCanCompileTestScripts = customScriptAssembly.HasPrecompiledReference(nUnitLibraryFilename)
|| customScriptAssembly.HasAssemblyFlag(AssemblyFlags.EditorOnly);
return assemblyCanCompileTestScripts;
}
var isImplicitEditorAssembly = FolderPathBelongsToImplicitEditorAssembly(folderPath);
return isImplicitEditorAssembly;
}
/// <summary>
/// Checks if the provided folder path is a special editor path that belongs to an implicit editor assembly.
/// </summary>
/// <param name="folderPath">The folder path to check.</param>
/// <returns>True if the folder path belongs to an implicit editor assembly; false otherwise.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="folderPath" /> string argument is null.</exception>
internal static bool FolderPathBelongsToImplicitEditorAssembly(string folderPath)
{
if (folderPath == null)
{
throw new ArgumentNullException(nameof(folderPath));
}
const char unityPathSeparator = '/';
var unityFormatPath = folderPath.Replace('\\', unityPathSeparator);
var folderComponents = unityFormatPath.Split(unityPathSeparator);
var folderComponentsIncludeEditorFolder = folderComponents.Any(n => n.ToLower().Equals("editor"));
return folderComponentsIncludeEditorFolder;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d92c578e28043ef95d4b703e008be64
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <summary>
/// Provides basic utility methods for creating assets in the active Project Browser folder path.
/// </summary>
internal interface IActiveFolderTemplateAssetCreator
{
/// <summary>
/// The active Project Browser folder path relative to the root project folder.
/// </summary>
/// <returns>The active folder path string.</returns>
string GetActiveFolderPath();
/// <summary>
/// Creates a new folder asset in the active folder path with assets defined by provided templates.
/// </summary>
/// <param name="defaultName">The default name of the folder.</param>
/// <param name="templateNames">The names of templates to be used when creating embedded assets.</param>
void CreateFolderWithTemplates(string defaultName, params string[] templateNames);
/// <summary>
/// Creates a new script asset in the active folder path defined by the provided template.
/// </summary>
/// <param name="defaultName">The default name of the new script asset.</param>
/// <param name="templatePath">The template to be used when creating the asset.</param>
void CreateScriptAssetFromTemplateFile(string defaultName, string templatePath);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 409ee14a63784dc0ab5d002081211814
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System;
using UnityEditor.Scripting.ScriptCompilation;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <summary>
/// Provides a wrapper for a Custom Script Assembly and exposes its basic properties.
/// </summary>
internal interface ICustomScriptAssembly
{
/// <summary>
/// Checks if the Custom Script Assembly is referencing the provided precompiled library.
/// </summary>
/// <param name="libraryFilename">The name of the precompiled library reference to be checked.</param>
/// <returns>True if the assembly references the provided precompiled library; false otherwise.</returns>
bool HasPrecompiledReference(string libraryFilename);
/// <summary>
/// Checks if the Custom Script Assembly has the provided <see cref="AssemblyFlags" /> value set.
/// </summary>
/// <param name="flag">The <see cref="AssemblyFlags" /> value to check against.</param>
/// <returns>True if the provided <paramref name="flag" /> value is set; false otherwise.</returns>
bool HasAssemblyFlag(AssemblyFlags flag);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 32829ea9e75c475295f73ff867e2f9d0
timeCreated: 1603203107

View File

@@ -0,0 +1,17 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <summary>
/// Provides mapping information from folder paths to their corresponding Custom Script Assembly scope.
/// </summary>
internal interface ICustomScriptAssemblyMappingFinder
{
/// <summary>
/// Finds the Custom Script Assembly associated with the provided folder path.
/// </summary>
/// <param name="folderPath">The folder path to check.</param>
/// <returns>The associated <see cref="ICustomScriptAssembly" />; null if none.</returns>
ICustomScriptAssembly FindCustomScriptAssemblyFromFolderPath(string folderPath);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0cd0deb81d984e58952ccd7e1dd6b2bb
timeCreated: 1603203104

View File

@@ -0,0 +1,20 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <summary>
/// Provides Test Script compilation context associated with project folder paths.
/// </summary>
internal interface IFolderPathTestCompilationContextProvider
{
/// <summary>
/// Checks if the provided folder path belongs to a Custom Test Assembly.
/// </summary>
bool FolderPathBelongsToCustomTestAssembly(string folderPath);
/// <summary>
/// Checks if the provided folder path belongs to an assembly capable of compiling Test Scripts.
/// </summary>
bool TestScriptWillCompileInFolderPath(string folderPath);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e5f4a89476c1448abc7e0a9719b13b36
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <summary>
/// Provides an interface for creating test assets from templates.
/// </summary>
internal interface ITestScriptAssetsCreator
{
/// <summary>
/// Creates a new folder in the active folder path with an associated Test Script Assembly definition.
/// </summary>
/// <param name="isEditorOnly">Should the assembly definition be editor-only?</param>
void AddNewFolderWithTestAssemblyDefinition(bool isEditorOnly = false);
/// <summary>
/// Checks if the active folder path already contains a Test Script Assembly definition.
/// </summary>
/// <returns>True if the active folder path contains a Test Script Assembly; false otherwise.</returns>
bool ActiveFolderContainsTestAssemblyDefinition();
/// <summary>
/// Adds a new Test Script asset in the active folder path.
/// </summary>
void AddNewTestScript();
/// <summary>
/// Checks if a Test Script asset can be compiled in the active folder path.
/// </summary>
/// <returns>True if a Test Script can be compiled in the active folder path; false otherwise.</returns>
bool TestScriptWillCompileInActiveFolder();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e7f72702c2f04b999739380ef9c0de5f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <summary>
/// The set of Menu Items dedicated to creating test assets: Test Scripts and Custom Test Assemblies.
/// </summary>
internal static class TestScriptAssetMenuItems
{
internal const string addNewFolderWithTestAssemblyDefinitionMenuItem = "Assets/Create/Testing/Tests Assembly Folder";
internal const string addNewTestScriptMenuItem = "Assets/Create/Testing/C# Test Script";
/// <summary>
/// Adds a new folder asset and an associated Custom Test Assembly in the active folder path.
/// </summary>
[MenuItem(addNewFolderWithTestAssemblyDefinitionMenuItem, false, 83)]
public static void AddNewFolderWithTestAssemblyDefinition()
{
TestScriptAssetsCreator.Instance.AddNewFolderWithTestAssemblyDefinition();
}
/// <summary>
/// Checks if it is possible to add a new Custom Test Assembly inside the active folder path.
/// </summary>
/// <returns>False if the active folder path already contains a Custom Test Assembly; true otherwise.</returns>
[MenuItem(addNewFolderWithTestAssemblyDefinitionMenuItem, true, 83)]
public static bool CanAddNewFolderWithTestAssemblyDefinition()
{
var testAssemblyAlreadyExists = TestScriptAssetsCreator.Instance.ActiveFolderContainsTestAssemblyDefinition();
return !testAssemblyAlreadyExists;
}
/// <summary>
/// Adds a new Test Script asset in the active folder path.
/// </summary>
[MenuItem(addNewTestScriptMenuItem, false, 83)]
public static void AddNewTestScript()
{
TestScriptAssetsCreator.Instance.AddNewTestScript();
}
/// <summary>
/// Checks if it is possible to add a new Test Script in the active folder path.
/// </summary>
/// <returns>True if a Test Script can be compiled in the active folder path; false otherwise.</returns>
[MenuItem(addNewTestScriptMenuItem, true, 83)]
public static bool CanAddNewTestScript()
{
var testScriptWillCompile = TestScriptAssetsCreator.Instance.TestScriptWillCompileInActiveFolder();
return testScriptWillCompile;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3c702cb84a2a4576bf275a76bc17f8e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
using System;
using System.IO;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <inheritdoc />
internal class TestScriptAssetsCreator : ITestScriptAssetsCreator
{
private const string k_AssemblyDefinitionEditModeTestTemplate = "92-Assembly Definition-NewEditModeTestAssembly.asmdef.txt";
internal const string assemblyDefinitionTestTemplate = "92-Assembly Definition-NewTestAssembly.asmdef.txt";
internal const string resourcesTemplatePath = "Resources/ScriptTemplates";
internal const string testScriptTemplate = "83-C# Script-NewTestScript.cs.txt";
internal const string defaultNewTestAssemblyFolderName = "Tests";
internal const string defaultNewTestScriptName = "NewTestScript.cs";
private static IFolderPathTestCompilationContextProvider s_FolderPathCompilationContext;
private static IActiveFolderTemplateAssetCreator s_ActiveFolderTemplateAssetCreator;
private static ITestScriptAssetsCreator s_Instance;
internal static IFolderPathTestCompilationContextProvider FolderPathContext
{
private get => s_FolderPathCompilationContext ?? (s_FolderPathCompilationContext = new FolderPathTestCompilationContextProvider());
set => s_FolderPathCompilationContext = value;
}
internal static IActiveFolderTemplateAssetCreator ActiveFolderTemplateAssetCreator
{
private get => s_ActiveFolderTemplateAssetCreator ?? (s_ActiveFolderTemplateAssetCreator = new ActiveFolderTemplateAssetCreator());
set => s_ActiveFolderTemplateAssetCreator = value;
}
internal static ITestScriptAssetsCreator Instance => s_Instance ?? (s_Instance = new TestScriptAssetsCreator());
private static string ActiveFolderPath => ActiveFolderTemplateAssetCreator.GetActiveFolderPath();
private static string ScriptTemplatesResourcesPath => Path.Combine(EditorApplication.applicationContentsPath, resourcesTemplatePath);
#if UNITY_2023_3_OR_NEWER
private static string ScriptTemplatePath => Path.Combine(ScriptTemplatesResourcesPath, AssetsMenuUtility.GetScriptTemplatePath(ScriptTemplate.CSharp_NewTestScript));
#else
private static string ScriptTemplatePath => Path.Combine(ScriptTemplatesResourcesPath, testScriptTemplate);
#endif
/// <inheritdoc />
public void AddNewFolderWithTestAssemblyDefinition(bool isEditorOnly = false)
{
#if UNITY_2023_3_OR_NEWER
var assemblyDefinitionTemplate =
AssetsMenuUtility.GetScriptTemplatePath(isEditorOnly
? ScriptTemplate.AsmDef_NewEditModeTestAssembly
: ScriptTemplate.AsmDef_NewTestAssembly);
#else
var assemblyDefinitionTemplate = isEditorOnly ? k_AssemblyDefinitionEditModeTestTemplate : assemblyDefinitionTestTemplate;
#endif
ActiveFolderTemplateAssetCreator.CreateFolderWithTemplates(defaultNewTestAssemblyFolderName, assemblyDefinitionTemplate);
}
/// <inheritdoc />
public void AddNewTestScript()
{
var destPath = Path.Combine(ActiveFolderTemplateAssetCreator.GetActiveFolderPath(), defaultNewTestScriptName);
ActiveFolderTemplateAssetCreator.CreateScriptAssetFromTemplateFile(destPath, ScriptTemplatePath);
}
/// <inheritdoc />
public bool ActiveFolderContainsTestAssemblyDefinition()
{
return FolderPathContext.FolderPathBelongsToCustomTestAssembly(ActiveFolderPath);
}
/// <inheritdoc />
public bool TestScriptWillCompileInActiveFolder()
{
return FolderPathContext.TestScriptWillCompileInFolderPath(ActiveFolderPath);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1bace19a170f47bb8d19645cfc580796
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 07ea0326ed848fb4489187cb58f96113
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
using UnityEngine.TestRunner.NUnitExtensions.Filters;
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
using TreeViewController = UnityEditor.IMGUI.Controls.TreeViewController<int>;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
using LazyTreeViewDataSource = UnityEditor.IMGUI.Controls.LazyTreeViewDataSource<int>;
using TreeViewUtility = UnityEditor.IMGUI.Controls.TreeViewUtility<int>;
using TreeViewSelectState = UnityEditor.IMGUI.Controls.TreeViewSelectState<int>;
using ITreeViewGUI = UnityEditor.IMGUI.Controls.ITreeViewGUI<int>;
using ITreeViewDragging = UnityEditor.IMGUI.Controls.ITreeViewDragging<int>;
using ITreeViewDataSource = UnityEditor.IMGUI.Controls.ITreeViewDataSource<int>;
using TreeViewGUI = UnityEditor.IMGUI.Controls.TreeViewGUI<int>;
using TreeViewDragging = UnityEditor.IMGUI.Controls.TreeViewDragging<int>;
using TreeViewDataSource = UnityEditor.IMGUI.Controls.TreeViewDataSource<int>;
using TreeViewItemAlphaNumericSort = UnityEditor.IMGUI.Controls.TreeViewItemAlphaNumericSort<int>;
using RenameOverlay = UnityEditor.RenameOverlay<int>;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal class TestTreeViewBuilder
{
internal struct TestCount
{
public int TotalTestCount;
public int TotalFailedTestCount;
}
public List<TestRunnerResult> results = new List<TestRunnerResult>();
public readonly Dictionary<string, TestTreeViewItem> m_treeFiltered = new Dictionary<string, TestTreeViewItem>();
private readonly Dictionary<string, TestRunnerResult> m_OldTestResults;
private readonly TestRunnerUIFilter m_UIFilter;
private readonly ITestAdaptor[] m_TestListRoots;
private readonly Dictionary<string, List<TestRunnerResult>> m_ChildrenResults;
private readonly bool m_runningOnPlatform;
private readonly List<string> m_AvailableCategories = new List<string>();
public string[] AvailableCategories
{
get { return m_AvailableCategories.Distinct().OrderBy(a => a).ToArray(); }
}
public TestTreeViewBuilder(ITestAdaptor[] tests, Dictionary<string, TestRunnerResult> oldTestResultResults, TestRunnerUIFilter uiFilter, bool runningOnPlatform)
{
m_AvailableCategories.Add(CategoryFilterExtended.k_DefaultCategory);
m_OldTestResults = oldTestResultResults;
m_ChildrenResults = new Dictionary<string, List<TestRunnerResult>>();
m_TestListRoots = tests;
m_UIFilter = uiFilter;
m_runningOnPlatform = runningOnPlatform;
}
public TreeViewItem BuildTreeView()
{
m_treeFiltered.Clear();
var rootItem = new TreeViewItem(int.MaxValue, 0, null, "Invisible Root Item");
foreach (var testRoot in m_TestListRoots)
{
ParseTestTree(0, rootItem, testRoot);
}
return rootItem;
}
private bool IsFilteredOutByUIFilter(ITestAdaptor test, TestRunnerResult result)
{
if (m_UIFilter.PassedHidden && result.resultStatus == TestRunnerResult.ResultStatus.Passed)
return true;
if (m_UIFilter.FailedHidden && (result.resultStatus == TestRunnerResult.ResultStatus.Failed || result.resultStatus == TestRunnerResult.ResultStatus.Inconclusive))
return true;
if (m_UIFilter.NotRunHidden && (result.resultStatus == TestRunnerResult.ResultStatus.NotRun || result.resultStatus == TestRunnerResult.ResultStatus.Skipped))
return true;
if (!string.IsNullOrEmpty(m_UIFilter.m_SearchString) && result.FullName.IndexOf(m_UIFilter.m_SearchString, StringComparison.InvariantCultureIgnoreCase) < 0)
return true;
if (m_UIFilter.CategoryFilter.Length > 0)
return !test.Categories.Any(category => m_UIFilter.CategoryFilter.Contains(category));
return false;
}
private TestCount ParseTestTree(int depth, TreeViewItem rootItem, ITestAdaptor testElement)
{
if (testElement == null)
{
return default;
}
var testCount = new TestCount();
m_AvailableCategories.AddRange(testElement.Categories);
var testElementId = testElement.UniqueName;
if (!testElement.HasChildren)
{
m_OldTestResults.TryGetValue(testElementId, out var result);
if (result != null && !m_runningOnPlatform &&
(result.ignoredOrSkipped
|| result.notRunnable
|| testElement.RunState == RunState.NotRunnable
|| testElement.RunState == RunState.Ignored
|| testElement.RunState == RunState.Skipped
)
)
{
// if the test was or becomes ignored or not runnable, we recreate the result in case it has changed
// It does not apply if we are running on a platform, as evaluation of runstate needs to be evaluated on the player.
result = null;
}
if (result == null)
{
result = new TestRunnerResult(testElement);
}
results.Add(result);
var test = new TestTreeViewItem(testElement, depth, rootItem);
if (!IsFilteredOutByUIFilter(testElement, result))
{
rootItem.AddChild(test);
if (!m_treeFiltered.ContainsKey(test.FullName))
m_treeFiltered.Add(test.FullName, test);
}
else
{
return testCount;
}
test.SetResult(result);
testCount.TotalTestCount = 1;
testCount.TotalFailedTestCount = result.resultStatus == TestRunnerResult.ResultStatus.Failed ? 1 : 0;
if (m_ChildrenResults != null && testElement.Parent != null)
{
m_ChildrenResults.TryGetValue(testElement.ParentUniqueName, out var resultList);
if (resultList != null)
{
resultList.Add(result);
}
else
{
resultList = new List<TestRunnerResult> {result};
m_ChildrenResults.Add(testElement.ParentUniqueName, resultList);
}
}
return testCount;
}
var groupResult = new TestRunnerResult(testElement);
results.Add(groupResult);
var group = new TestTreeViewItem(testElement, depth, rootItem);
depth++;
foreach (var child in testElement.Children)
{
var childTestCount = ParseTestTree(depth, group, child);
testCount.TotalTestCount += childTestCount.TotalTestCount;
testCount.TotalFailedTestCount += childTestCount.TotalFailedTestCount;
}
if (testElement.IsTestAssembly && !testElement.HasChildren)
{
return testCount;
}
if (group.hasChildren)
rootItem.AddChild(group);
group.TotalChildrenCount = testCount.TotalTestCount;
group.TotalSuccessChildrenCount = testCount.TotalFailedTestCount;
groupResult.CalculateParentResult(testElementId, m_ChildrenResults);
groupResult.CalculateAndSetParentDuration(testElementId, m_ChildrenResults);
group.SetResult(groupResult);
return testCount;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e17c88b021c2a4c409b3f15b0d80ac62
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 68cb547af0187634aad591a09c01cd5b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal static class Icons
{
public static readonly Texture2D s_FailImg;
public static readonly Texture2D s_IgnoreImg;
public static readonly Texture2D s_SuccessImg;
public static readonly Texture2D s_UnknownImg;
public static readonly Texture2D s_InconclusiveImg;
public static readonly Texture2D s_StopwatchImg;
static Icons()
{
s_FailImg = EditorGUIUtility.IconContent("TestFailed").image as Texture2D;
s_IgnoreImg = EditorGUIUtility.IconContent("TestIgnored").image as Texture2D;
s_SuccessImg = EditorGUIUtility.IconContent("TestPassed").image as Texture2D;
s_UnknownImg = EditorGUIUtility.IconContent("TestNormal").image as Texture2D;
s_InconclusiveImg = EditorGUIUtility.IconContent("TestInconclusive").image as Texture2D;
s_StopwatchImg = EditorGUIUtility.IconContent("TestStopwatch").image as Texture2D;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 27769e9b00b038d47aefe306a4d20bec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using UnityEditor.IMGUI.Controls;
using UnityEditor.TestTools.TestRunner.Api;
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
using TreeViewController = UnityEditor.IMGUI.Controls.TreeViewController<int>;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
using LazyTreeViewDataSource = UnityEditor.IMGUI.Controls.LazyTreeViewDataSource<int>;
using TreeViewUtility = UnityEditor.IMGUI.Controls.TreeViewUtility<int>;
using TreeViewSelectState = UnityEditor.IMGUI.Controls.TreeViewSelectState<int>;
using ITreeViewGUI = UnityEditor.IMGUI.Controls.ITreeViewGUI<int>;
using ITreeViewDragging = UnityEditor.IMGUI.Controls.ITreeViewDragging<int>;
using ITreeViewDataSource = UnityEditor.IMGUI.Controls.ITreeViewDataSource<int>;
using TreeViewGUI = UnityEditor.IMGUI.Controls.TreeViewGUI<int>;
using TreeViewDragging = UnityEditor.IMGUI.Controls.TreeViewDragging<int>;
using TreeViewDataSource = UnityEditor.IMGUI.Controls.TreeViewDataSource<int>;
using TreeViewItemAlphaNumericSort = UnityEditor.IMGUI.Controls.TreeViewItemAlphaNumericSort<int>;
using RenameOverlay = UnityEditor.RenameOverlay<int>;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal class TestListTreeViewDataSource : TreeViewDataSource
{
private bool m_ExpandTreeOnCreation;
private readonly TestListGUI m_TestListGUI;
private ITestAdaptor[] m_RootTests;
public TestListTreeViewDataSource(TreeViewController testListTree, TestListGUI testListGUI, ITestAdaptor[] rootTests) : base(testListTree)
{
showRootItem = false;
rootIsCollapsable = false;
m_TestListGUI = testListGUI;
m_RootTests = rootTests;
}
public void UpdateRootTest(ITestAdaptor[] rootTests)
{
m_RootTests = rootTests;
}
public override void FetchData()
{
var testListBuilder = new TestTreeViewBuilder(m_RootTests, m_TestListGUI.ResultsByKey, m_TestListGUI.m_TestRunnerUIFilter, m_TestListGUI.m_RunOnPlatform);
m_RootItem = testListBuilder.BuildTreeView();
SetExpanded(m_RootItem, true);
if (m_RootItem.hasChildren && m_RootItem.children.Count == 1)
SetExpanded(m_RootItem.children[0], true);
if (m_ExpandTreeOnCreation)
SetExpandedWithChildren(m_RootItem, true);
m_TestListGUI.newResultList = new List<TestRunnerResult>(testListBuilder.results);
m_TestListGUI.filteredTree = testListBuilder.m_treeFiltered;
m_TestListGUI.m_TestRunnerUIFilter.availableCategories = testListBuilder.AvailableCategories;
m_NeedRefreshRows = true;
}
public override bool IsRenamingItemAllowed(TreeViewItem item)
{
return false;
}
public void ExpandTreeOnCreation()
{
m_ExpandTreeOnCreation = true;
}
public override bool IsExpandable(TreeViewItem item)
{
if (item is TestTreeViewItem)
return ((TestTreeViewItem)item).IsGroupNode;
return base.IsExpandable(item);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ce87c287371edde43a4b5fcfdee7b9ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,27 @@
using System;
using UnityEditor.IMGUI.Controls;
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
using TreeViewController = UnityEditor.IMGUI.Controls.TreeViewController<int>;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
using LazyTreeViewDataSource = UnityEditor.IMGUI.Controls.LazyTreeViewDataSource<int>;
using TreeViewUtility = UnityEditor.IMGUI.Controls.TreeViewUtility<int>;
using TreeViewSelectState = UnityEditor.IMGUI.Controls.TreeViewSelectState<int>;
using ITreeViewGUI = UnityEditor.IMGUI.Controls.ITreeViewGUI<int>;
using ITreeViewDragging = UnityEditor.IMGUI.Controls.ITreeViewDragging<int>;
using ITreeViewDataSource = UnityEditor.IMGUI.Controls.ITreeViewDataSource<int>;
using TreeViewGUI = UnityEditor.IMGUI.Controls.TreeViewGUI<int>;
using TreeViewDragging = UnityEditor.IMGUI.Controls.TreeViewDragging<int>;
using TreeViewDataSource = UnityEditor.IMGUI.Controls.TreeViewDataSource<int>;
using TreeViewItemAlphaNumericSort = UnityEditor.IMGUI.Controls.TreeViewItemAlphaNumericSort<int>;
using RenameOverlay = UnityEditor.RenameOverlay<int>;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal class TestListTreeViewGUI : TreeViewGUI
{
public TestListTreeViewGUI(TreeViewController testListTree) : base(testListTree)
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52c907c81459f324497af504b84fd557
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor.IMGUI.Controls;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
using TreeViewController = UnityEditor.IMGUI.Controls.TreeViewController<int>;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
using LazyTreeViewDataSource = UnityEditor.IMGUI.Controls.LazyTreeViewDataSource<int>;
using TreeViewUtility = UnityEditor.IMGUI.Controls.TreeViewUtility<int>;
using TreeViewSelectState = UnityEditor.IMGUI.Controls.TreeViewSelectState<int>;
using ITreeViewGUI = UnityEditor.IMGUI.Controls.ITreeViewGUI<int>;
using ITreeViewDragging = UnityEditor.IMGUI.Controls.ITreeViewDragging<int>;
using ITreeViewDataSource = UnityEditor.IMGUI.Controls.ITreeViewDataSource<int>;
using TreeViewGUI = UnityEditor.IMGUI.Controls.TreeViewGUI<int>;
using TreeViewDragging = UnityEditor.IMGUI.Controls.TreeViewDragging<int>;
using TreeViewDataSource = UnityEditor.IMGUI.Controls.TreeViewDataSource<int>;
using TreeViewItemAlphaNumericSort = UnityEditor.IMGUI.Controls.TreeViewItemAlphaNumericSort<int>;
using RenameOverlay = UnityEditor.RenameOverlay<int>;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal sealed class TestTreeViewItem : TreeViewItem
{
public TestRunnerResult result;
internal ITestAdaptor m_Test;
public Type type;
public MethodInfo method;
private const int k_ResultTestMaxLength = 15000;
public bool IsGroupNode { get { return m_Test.IsSuite; } }
public string FullName { get { return m_Test.FullName; } }
public string UniqueName { get { return m_Test.UniqueName; } }
public override string displayName
{
get => $"{base.displayName}{(hasChildren ? $" ({TotalChildrenCount} tests) {(TotalSuccessChildrenCount > 0 ? $" {TotalSuccessChildrenCount} tests failed" : null)}" : null)}";
set => base.displayName = value;
}
public string GetAssemblyName()
{
var test = m_Test;
while (test != null)
{
if (test.IsTestAssembly)
{
return test.FullName;
}
test = test.Parent;
}
return null;
}
public IEnumerable<ITestAdaptor> GetMinimizedSelectedTree()
{
if (!m_Test.HasChildren)
{
yield return m_Test;
yield break;
}
var minimizedDescendants = children.OfType<TestTreeViewItem>().SelectMany(c => c.GetMinimizedSelectedTree()).ToArray();
var includeChildren = minimizedDescendants.Count(c => c.Parent == m_Test);
if (includeChildren == m_Test.Children.Count())
{
// All children are included in the filter, so we can just return the parent
yield return m_Test;
yield break;
}
foreach (var child in minimizedDescendants)
{
yield return child;
}
}
public TestTreeViewItem(ITestAdaptor test, int depth, TreeViewItem parent)
: base(GetId(test), depth, parent, test.Name)
{
m_Test = test;
if (test.TypeInfo != null)
{
type = test.TypeInfo.Type;
}
if (test.Method != null)
{
method = test.Method.MethodInfo;
}
displayName = test.Name.Replace("\n", "");
icon = Icons.s_UnknownImg;
}
public int TotalChildrenCount { get; set; }
public int TotalSuccessChildrenCount { get; set; }
private static int GetId(ITestAdaptor test)
{
return test.UniqueName.GetHashCode();
}
public void SetResult(TestRunnerResult testResult)
{
result = testResult;
result.SetResultChangedCallback(ResultUpdated);
ResultUpdated(result);
}
public string GetResultText()
{
if (result.resultStatus == TestRunnerResult.ResultStatus.NotRun)
{
if (result.ignoredOrSkipped)
{
return result.messages;
}
return string.Empty;
}
var durationString = String.Format("{0:0.000}", result.duration);
var sb = new StringBuilder(string.Format("{0} ({1}s)", displayName.Trim(), durationString));
if (!string.IsNullOrEmpty(result.description))
{
sb.AppendFormat("\n{0}", result.description);
}
if (!string.IsNullOrEmpty(result.messages))
{
sb.Append("\n---\n");
sb.Append(result.messages);
}
if (!string.IsNullOrEmpty(result.stacktrace))
{
sb.Append("\n---\n");
sb.Append(StacktraceWithHyperlinks(result.stacktrace));
}
if (!string.IsNullOrEmpty(result.output))
{
sb.Append("\n---\n");
sb.Append(result.output);
}
if (sb.Length > k_ResultTestMaxLength)
{
sb.Length = k_ResultTestMaxLength;
sb.AppendFormat("...\n\n---MESSAGE TRUNCATED AT {0} CHARACTERS---", k_ResultTestMaxLength);
}
return sb.ToString().Trim();
}
private void ResultUpdated(TestRunnerResult testResult)
{
switch (testResult.resultStatus)
{
case TestRunnerResult.ResultStatus.Passed:
icon = Icons.s_SuccessImg;
break;
case TestRunnerResult.ResultStatus.Failed:
icon = Icons.s_FailImg;
break;
case TestRunnerResult.ResultStatus.Inconclusive:
icon = Icons.s_InconclusiveImg;
break;
case TestRunnerResult.ResultStatus.Skipped:
icon = Icons.s_IgnoreImg;
break;
default:
if (testResult.ignoredOrSkipped)
{
icon = Icons.s_IgnoreImg;
}
else if (testResult.notRunnable)
{
icon = Icons.s_FailImg;
}
else
{
icon = Icons.s_UnknownImg;
}
break;
}
}
private static string StacktraceWithHyperlinks(string stacktraceText)
{
StringBuilder textWithHyperlinks = new StringBuilder();
var lines = stacktraceText.Split(new string[] { "\n" }, StringSplitOptions.None);
for (int i = 0; i < lines.Length; ++i)
{
string textBeforeFilePath = "] in ";
int filePathIndex = lines[i].IndexOf(textBeforeFilePath, StringComparison.Ordinal);
if (filePathIndex > 0)
{
filePathIndex += textBeforeFilePath.Length;
if (lines[i][filePathIndex] != '<') // sometimes no url is given, just an id between <>, we can't do an hyperlink
{
string filePathPart = lines[i].Substring(filePathIndex);
int lineIndex = filePathPart.LastIndexOf(":", StringComparison.Ordinal); // LastIndex because the url can contain ':' ex:"C:"
if (lineIndex > 0)
{
string lineString = filePathPart.Substring(lineIndex + 1);
string filePath = filePathPart.Substring(0, lineIndex);
#if UNITY_2021_3_OR_NEWER
var displayedPath = Path.GetRelativePath(Directory.GetCurrentDirectory(), filePath);
#else
var displayedPath = filePath;
#endif
textWithHyperlinks.Append($"{lines[i].Substring(0, filePathIndex)}<color=#40a0ff><a href=\"{filePath}\" line=\"{lineString}\">{displayedPath}:{lineString}</a></color>\n");
continue; // continue to evade the default case
}
}
}
// default case if no hyperlink : we just write the line
textWithHyperlinks.Append(lines[i] + "\n");
}
// Remove the last \n
if (textWithHyperlinks.Length > 0) // textWithHyperlinks always ends with \n if it is not empty
textWithHyperlinks.Remove(textWithHyperlinks.Length - 1, 1);
return textWithHyperlinks.ToString();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ce567ddbf30368344bc7b80e20cac36e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using System;
using UnityEditor.TestTools.TestRunner.GUI.Controls;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI
{
internal static class TestRunnerGUI
{
private static Styles s_Styles;
private static Styles Style => s_Styles ?? (s_Styles = new Styles());
internal static void TestPlatformSelectionDropDown(ISelectionDropDownContentProvider contentProvider)
{
var text = Style.TestPlatformButtonString;
for (int i = 0; i < contentProvider.Count; i++)
{
if (contentProvider.IsSelected(i))
{
text += " " + contentProvider.GetName(i);
break;
}
}
var content = new GUIContent(text);
SelectionDropDown(contentProvider, content, GUILayout.Width(EditorStyles.toolbarDropDown.CalcSize(content).x));
}
internal static void CategorySelectionDropDown(ISelectionDropDownContentProvider contentProvider)
{
SelectionDropDown(contentProvider, Style.CategoryButtonContent, GUILayout.Width(Style.CategoryButtonWidth));
}
private static void SelectionDropDown(ISelectionDropDownContentProvider listContentProvider, GUIContent buttonContent,
params GUILayoutOption[] options)
{
var rect = EditorGUILayout.GetControlRect(false, EditorGUI.kSingleLineHeight, Styles.DropdownButton, options);
if (!EditorGUI.DropdownButton(rect, buttonContent, FocusType.Passive, Styles.DropdownButton))
{
return;
}
var selectionDropDown = new SelectionDropDown(listContentProvider);
PopupWindow.Show(rect, selectionDropDown);
}
private class Styles
{
public static readonly GUIStyle DropdownButton = EditorStyles.toolbarDropDown;
public readonly string TestPlatformButtonString = "Run Location:";
public readonly GUIContent CategoryButtonContent = new GUIContent("Category");
public readonly float CategoryButtonWidth;
public Styles()
{
CategoryButtonWidth = DropdownButton.CalcSize(CategoryButtonContent).x;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5a0dfda606a24736913c00edd76e55f6
timeCreated: 1600071499

View File

@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI
{
[Serializable]
internal class TestRunnerResult : UITestRunnerFilter.IClearableResult
{
public string id;
public string uniqueId;
public string name;
public string fullName;
public ResultStatus resultStatus = ResultStatus.NotRun;
public float duration;
public string messages;
public string output;
public string stacktrace;
public bool notRunnable;
public bool ignoredOrSkipped;
public string description;
public bool isSuite;
public List<string> categories;
public string parentId;
public string parentUniqueId;
//This field is suppose to mark results from before domain reload
//Such result is outdated because the code might haev changed
//This field will get reset every time a domain reload happens
[NonSerialized]
public bool notOutdated;
protected Action<TestRunnerResult> m_OnResultUpdate;
internal TestRunnerResult(ITestAdaptor test)
{
id = test.Id;
uniqueId = test.UniqueName;
fullName = test.FullName;
name = test.Name;
description = test.Description;
isSuite = test.IsSuite;
ignoredOrSkipped = test.RunState == RunState.Ignored || test.RunState == RunState.Skipped;
notRunnable = test.RunState == RunState.NotRunnable;
if (ignoredOrSkipped)
{
resultStatus = ResultStatus.Skipped;
messages = test.SkipReason;
}
if (notRunnable)
{
resultStatus = ResultStatus.Failed;
messages = test.SkipReason;
}
categories = test.Categories.ToList();
parentId = test.ParentId;
parentUniqueId = test.ParentUniqueName;
}
internal TestRunnerResult(ITestResultAdaptor testResult) : this(testResult.Test)
{
notOutdated = true;
messages = testResult.Message;
output = testResult.Output;
stacktrace = testResult.StackTrace;
duration = (float)testResult.Duration;
if (testResult.Test.IsSuite && testResult.ResultState == "Ignored")
{
resultStatus = ResultStatus.Passed;
}
else
{
resultStatus = ParseNUnitResultStatus(testResult.TestStatus);
}
}
public void CalculateParentResult(string parentId, IDictionary<string, List<TestRunnerResult>> results)
{
if (results == null) return;
results.TryGetValue(parentId , out var childrenResult);
if (childrenResult == null) return;
if (childrenResult.TrueForAll(x => x.resultStatus == ResultStatus.Passed)) resultStatus = ResultStatus.Passed;
if (childrenResult.TrueForAll(x => x.resultStatus == ResultStatus.Skipped)) resultStatus = ResultStatus.Skipped;
else if (childrenResult.Any(x => x.resultStatus == ResultStatus.Skipped))
{
resultStatus = ResultStatus.Passed;
}
if (childrenResult.Any(x => x.resultStatus == ResultStatus.Inconclusive)) resultStatus = ResultStatus.Inconclusive;
if (childrenResult.Any(x => x.resultStatus == ResultStatus.Failed)) resultStatus = ResultStatus.Failed;
if (childrenResult.Any(x => x.resultStatus == ResultStatus.NotRun)) resultStatus = ResultStatus.NotRun;
UpdateParentResult(results);
}
public void CalculateAndSetParentDuration(string parentId, IDictionary<string, List<TestRunnerResult>> results)
{
if (results == null) return;
results.TryGetValue(parentId , out var childrenResult);
if (childrenResult == null) return;
var totalDuration = childrenResult.Sum(x => x.duration);
duration = (float)totalDuration;
}
private void UpdateParentResult(IDictionary<string, List<TestRunnerResult>> results)
{
if (string.IsNullOrEmpty(parentUniqueId)) return;
results.TryGetValue(parentUniqueId, out var parentResultList);
if (parentResultList != null && parentResultList.Count > 0)
{
parentResultList.Add(this);
}
else
{
results.Add(parentUniqueId, new List<TestRunnerResult> {this});
}
}
public void Update(TestRunnerResult result)
{
if (ReferenceEquals(result, null))
return;
resultStatus = result.resultStatus;
duration = result.duration;
messages = result.messages;
output = result.output;
stacktrace = result.stacktrace;
ignoredOrSkipped = result.ignoredOrSkipped;
notRunnable = result.notRunnable;
description = result.description;
notOutdated = result.notOutdated;
if (m_OnResultUpdate != null)
{
m_OnResultUpdate(this);
}
}
public void SetResultChangedCallback(Action<TestRunnerResult> resultUpdated)
{
m_OnResultUpdate = resultUpdated;
}
[Serializable]
internal enum ResultStatus
{
NotRun,
Passed,
Failed,
Inconclusive,
Skipped
}
private static ResultStatus ParseNUnitResultStatus(TestStatus status)
{
switch (status)
{
case TestStatus.Passed:
return ResultStatus.Passed;
case TestStatus.Failed:
return ResultStatus.Failed;
case TestStatus.Inconclusive:
return ResultStatus.Inconclusive;
case TestStatus.Skipped:
return ResultStatus.Skipped;
default:
return ResultStatus.NotRun;
}
}
public override string ToString()
{
return string.Format("{0} ({1})", name, fullName);
}
public string Id { get { return uniqueId; } }
public string FullName { get { return fullName; } }
public string ParentId { get { return parentUniqueId; } }
public bool IsSuite { get { return isSuite; } }
public List<string> Categories { get { return categories; } }
public void Clear()
{
resultStatus = ResultStatus.NotRun;
stacktrace = string.Empty;
duration = 0.0f;
if (m_OnResultUpdate != null)
m_OnResultUpdate(this);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a04a45bbed9e1714f9902fc9443669b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEditor.TestTools.TestRunner.GUI.Controls;
using UnityEngine;
namespace UnityEditor.TestTools.TestRunner.GUI
{
[Serializable]
internal class TestRunnerUIFilter
{
private static class DebouncedSearch
{
private const double k_DebounceDuration = 0.2;
private static double s_EndTime;
private static bool s_IsHooked;
static TestRunnerUIFilter s_Target;
internal static void Hook(TestRunnerUIFilter target)
{
s_EndTime = EditorApplication.timeSinceStartup + k_DebounceDuration;
s_Target = target;
if (s_IsHooked)
return;
EditorApplication.update += Update;
s_IsHooked = true;
}
private static void Update()
{
if (EditorApplication.timeSinceStartup > s_EndTime)
{
ApplySearchAndUnhook();
}
}
private static void ApplySearchAndUnhook()
{
s_Target.SearchStringChanged(s_Target.m_SearchString);
if (String.IsNullOrEmpty(s_Target.m_SearchString))
s_Target.SearchStringCleared();
EditorApplication.update -= Update;
s_IsHooked = false;
}
}
private int m_PassedCount;
private int m_FailedCount;
private int m_NotRunCount;
private int m_InconclusiveCount;
private int m_SkippedCount;
public int PassedCount { get { return m_PassedCount; } }
public int FailedCount { get { return m_FailedCount + m_InconclusiveCount; } }
public int NotRunCount { get { return m_NotRunCount + m_SkippedCount; } }
[SerializeField]
public bool PassedHidden;
[SerializeField]
public bool FailedHidden;
[SerializeField]
public bool NotRunHidden;
[SerializeField]
public string m_SearchString;
[SerializeField]
private string[] selectedCategories = new string[0];
public string[] availableCategories = new string[0];
private GUIContent m_SucceededBtn;
private GUIContent m_FailedBtn;
private GUIContent m_NotRunBtn;
public Action RebuildTestList;
public Action UpdateTestTreeRoots;
public Action<string> SearchStringChanged;
public Action SearchStringCleared;
public bool IsFiltering
{
get
{
return !string.IsNullOrEmpty(m_SearchString) || PassedHidden || FailedHidden || NotRunHidden ||
(selectedCategories != null && selectedCategories.Length > 0);
}
}
public string[] CategoryFilter
{
get { return selectedCategories; }
}
public void UpdateCounters(List<TestRunnerResult> resultList, Dictionary<string, TestTreeViewItem> filteredTree)
{
m_PassedCount = m_FailedCount = m_NotRunCount = m_InconclusiveCount = m_SkippedCount = 0;
foreach (var result in resultList)
{
if (result.isSuite)
continue;
if (filteredTree != null && !filteredTree.ContainsKey(result.fullName))
continue;
switch (result.resultStatus)
{
case TestRunnerResult.ResultStatus.Passed:
m_PassedCount++;
break;
case TestRunnerResult.ResultStatus.Failed:
m_FailedCount++;
break;
case TestRunnerResult.ResultStatus.Inconclusive:
m_InconclusiveCount++;
break;
case TestRunnerResult.ResultStatus.Skipped:
m_SkippedCount++;
break;
case TestRunnerResult.ResultStatus.NotRun:
default:
m_NotRunCount++;
break;
}
}
var succeededTooltip = string.Format("Show tests that succeeded\n{0} succeeded", m_PassedCount);
m_SucceededBtn = new GUIContent(PassedCount.ToString(), Icons.s_SuccessImg, succeededTooltip);
var failedTooltip = string.Format("Show tests that failed\n{0} failed\n{1} inconclusive", m_FailedCount, m_InconclusiveCount);
m_FailedBtn = new GUIContent(FailedCount.ToString(), Icons.s_FailImg, failedTooltip);
var notRunTooltip = string.Format("Show tests that didn't run\n{0} didn't run\n{1} skipped or ignored", m_NotRunCount, m_SkippedCount);
m_NotRunBtn = new GUIContent(NotRunCount.ToString(), Icons.s_UnknownImg, notRunTooltip);
}
public void Draw()
{
EditorGUI.BeginChangeCheck();
if (m_SearchString == null)
{
m_SearchString = "";
}
m_SearchString = EditorGUILayout.ToolbarSearchField(m_SearchString);
if (EditorGUI.EndChangeCheck() && SearchStringChanged != null)
{
DebouncedSearch.Hook(this);
}
if (availableCategories != null && availableCategories.Any())
{
TestRunnerGUI.CategorySelectionDropDown(BuildCategorySelectionProvider());
}
else
{
EditorGUILayout.Popup(0, new[] { "<No categories available>" }, EditorStyles.toolbarDropDown, GUILayout.MaxWidth(150));
}
EditorGUI.BeginChangeCheck();
if (m_SucceededBtn != null)
{
PassedHidden = !GUILayout.Toggle(!PassedHidden, m_SucceededBtn, EditorStyles.toolbarButton, GUILayout.MaxWidth(GetMaxWidth(PassedCount)));
}
if (m_FailedBtn != null)
{
FailedHidden = !GUILayout.Toggle(!FailedHidden, m_FailedBtn, EditorStyles.toolbarButton, GUILayout.MaxWidth(GetMaxWidth(FailedCount)));
}
if (m_NotRunBtn != null)
{
NotRunHidden = !GUILayout.Toggle(!NotRunHidden, m_NotRunBtn, EditorStyles.toolbarButton, GUILayout.MaxWidth(GetMaxWidth(NotRunCount)));
}
if (EditorGUI.EndChangeCheck() && RebuildTestList != null)
{
RebuildTestList();
}
}
public void OnModeGUI()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
{
// TODO: Tabs for editmode, playmode and player
}
EditorGUILayout.EndHorizontal();
}
private ISelectionDropDownContentProvider BuildCategorySelectionProvider()
{
var itemProvider = new MultiValueContentProvider<string>(availableCategories, selectedCategories,
categories =>
{
selectedCategories = categories;
UpdateTestTreeRoots();
});
return itemProvider;
}
private static int GetMaxWidth(int count)
{
if (count < 10)
return 33;
return count < 100 ? 40 : 47;
}
public void Clear()
{
PassedHidden = false;
FailedHidden = false;
NotRunHidden = false;
selectedCategories = new string[0];
m_SearchString = "";
if (SearchStringChanged != null)
{
SearchStringChanged(m_SearchString);
}
if (SearchStringCleared != null)
{
SearchStringCleared();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 15f870c6975ad6449b5b52514b90dc2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace UnityEditor.TestTools.TestRunner.GUI
{
[Serializable]
internal class UITestRunnerFilter
{
#pragma warning disable 649
public string[] assemblyNames;
public string[] groupNames;
public string[] categoryNames;
public string[] testNames;
public bool synchronousOnly;
public static string AssemblyNameFromPath(string path)
{
string output = Path.GetFileName(path);
if (output != null && output.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
return output.Substring(0, output.Length - 4);
return output;
}
private bool CategoryMatches(IEnumerable<string> categories)
{
if (categoryNames == null || categoryNames.Length == 0)
return true;
foreach (string category in categories)
{
if (categoryNames.Contains(category))
return true;
}
return false;
}
private bool IDMatchesAssembly(string id)
{
if (AreOptionalFiltersEmpty())
return true;
if (assemblyNames == null || assemblyNames.Length == 0)
return true;
int openingBracket = id.IndexOf('[');
int closingBracket = id.IndexOf(']');
if (openingBracket >= 0 && openingBracket < id.Length && closingBracket > openingBracket &&
openingBracket < id.Length)
{
//Some assemblies are absolute and explicitly part of the test ID e.g.
//"[/path/to/assembly-name.dll][rest of ID ...]"
//While some are minimal assembly names e.g.
//"[assembly-name][rest of ID ...]"
//Strip them down to just the assembly name
string assemblyNameFromID =
AssemblyNameFromPath(id.Substring(openingBracket + 1, closingBracket - openingBracket - 1));
foreach (string assemblyName in assemblyNames)
{
if (assemblyName.Equals(assemblyNameFromID, StringComparison.OrdinalIgnoreCase))
return true;
}
}
return false;
}
private bool NameMatches(string name)
{
if (AreOptionalFiltersEmpty())
return true;
if (groupNames == null || groupNames.Length == 0)
return true;
foreach (var nameFromFilter in groupNames)
{
//Strict regex match for test group name on its own
if (Regex.IsMatch(name, nameFromFilter))
return true;
//Match test names that end with Parameterized test values and full nunit generated test names that have . separators
var regex = nameFromFilter.TrimEnd('$') + @"[\.|\(.*\)]";
if (Regex.IsMatch(name, regex))
return true;
}
return false;
}
private bool AreOptionalFiltersEmpty()
{
if (assemblyNames != null && assemblyNames.Length != 0)
return false;
if (groupNames != null && groupNames.Length != 0)
return false;
if (testNames != null && testNames.Length != 0)
return false;
return true;
}
private bool NameMatchesExactly(string name, HashSet<string> nameLookup)
{
if (AreOptionalFiltersEmpty())
return true;
if (testNames == null || testNames.Length == 0)
return true;
return nameLookup.Contains(name);
}
private static void ClearAncestors(Dictionary<string, IClearableResult> newResultList, string parentID)
{
while (!string.IsNullOrEmpty(parentID) && newResultList.TryGetValue(parentID, out var parent))
{
parent.Clear();
parentID = parent.ParentId;
}
}
public void ClearResults(Dictionary<string, IClearableResult> newResultList)
{
var nameLookup = new HashSet<string>(testNames ?? new string[0]);
foreach (var kvp in newResultList)
{
var result = kvp.Value;
if (!result.IsSuite && CategoryMatches(result.Categories))
{
if (IDMatchesAssembly(result.Id) && NameMatches(result.FullName) &&
NameMatchesExactly(result.FullName, nameLookup))
{
result.Clear();
ClearAncestors(newResultList, result.ParentId);
}
}
}
}
internal interface IClearableResult
{
string Id { get; }
string FullName { get; }
string ParentId { get; }
bool IsSuite { get; }
List<string> Categories { get; }
void Clear();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8069e1fc631e461ababf11f19a9c0df3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c5535d742ea2e4941850b421f9c70a1f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,883 @@
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;
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
using TreeViewController = UnityEditor.IMGUI.Controls.TreeViewController<int>;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
using LazyTreeViewDataSource = UnityEditor.IMGUI.Controls.LazyTreeViewDataSource<int>;
using TreeViewUtility = UnityEditor.IMGUI.Controls.TreeViewUtility<int>;
using TreeViewSelectState = UnityEditor.IMGUI.Controls.TreeViewSelectState<int>;
using ITreeViewGUI = UnityEditor.IMGUI.Controls.ITreeViewGUI<int>;
using ITreeViewDragging = UnityEditor.IMGUI.Controls.ITreeViewDragging<int>;
using ITreeViewDataSource = UnityEditor.IMGUI.Controls.ITreeViewDataSource<int>;
using TreeViewGUI = UnityEditor.IMGUI.Controls.TreeViewGUI<int>;
using TreeViewDragging = UnityEditor.IMGUI.Controls.TreeViewDragging<int>;
using TreeViewDataSource = UnityEditor.IMGUI.Controls.TreeViewDataSource<int>;
using TreeViewItemAlphaNumericSort = UnityEditor.IMGUI.Controls.TreeViewItemAlphaNumericSort<int>;
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
using RenameOverlay = UnityEditor.RenameOverlay<int>;
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<string, TestTreeViewItem> filteredTree { get; set; }
public List<TestRunnerResult> newResultList
{
get { return m_NewResultList; }
set
{
m_NewResultList = value;
m_ResultByKey = null;
}
}
[SerializeField]
private List<TestRunnerResult> m_NewResultList = new List<TestRunnerResult>();
private Dictionary<string, TestRunnerResult> m_ResultByKey;
internal Dictionary<string, TestRunnerResult> ResultsByKey
{
get
{
if (m_ResultByKey == null)
{
m_ResultByKey = new Dictionary<string, TestRunnerResult>();
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<TestRunnerResult> m_QueuedResults = new List<TestRunnerResult>();
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;
TestRunnerWindow.s_Instance.Repaint();
}
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();
}
else if (m_TestMode == TestMode.PlayMode)
{
// Note, the following empty vertical area is inserted to give a different imgui id to the search bar.
// Otherwise imgui will threat the EditMode and PlayMode search bar as the same input.
EditorGUILayout.BeginVertical();
EditorGUILayout.EndVertical();
}
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) || !TestRunnerApi.IsRunning(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<TestRunnerApi>();
}
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<UITestRunnerFilter.IClearableResult>().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<TestRunnerApi>();
}
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()};
}
if (runFilter == RunFilterType.RunFailed)
{
return new[]
{
new UITestRunnerFilter()
{
testNames = ResultsByKey
.Where(resultPair => !resultPair.Value.isSuite && resultPair.Value.resultStatus == TestRunnerResult.ResultStatus.Failed)
.Select(resultPair => resultPair.Value.FullName)
.ToArray()
}
};
}
var includedIds = GetIdsIncludedInRunFilter(runFilter, specificTests);
var testsInFilter = includedIds.Select(id => m_TestListTree.FindItem(id)).Cast<TestTreeViewItem>()
.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<UITestRunnerFilter>();
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<int> 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,
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b8abb41ceb6f62c45a00197ae59224c1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: