This commit is contained in:
2025-01-17 13:10:42 +01:00
commit 4536213c91
15115 changed files with 1442174 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,26 @@
using System;
namespace UnityEditor.TestTools.TestRunner.GUI.TestAssets
{
/// <inheritdoc />
internal class ActiveFolderTemplateAssetCreator : IActiveFolderTemplateAssetCreator
{
/// <inheritdoc />
public string GetActiveFolderPath()
{
return ProjectWindowUtil.GetActiveFolderPath();
}
/// <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,169 @@
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;
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);
group.SetResult(groupResult);
return testCount;
}
}
}

Some files were not shown because too many files have changed in this diff Show More