451 lines
16 KiB
C#
451 lines
16 KiB
C#
using NUnit.Framework;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using UnityEngine.TestTools;
|
|
using UnityEngine.UI;
|
|
|
|
namespace RuntimeUnitTestToolkit
|
|
{
|
|
public class UnitTestRunner : MonoBehaviour
|
|
{
|
|
// object is IEnumerator or Func<IEnumerator>
|
|
Dictionary<string, List<KeyValuePair<string, object>>> tests = new Dictionary<string, List<KeyValuePair<string, object>>>();
|
|
|
|
List<Pair> additionalActionsOnFirst = new List<Pair>();
|
|
|
|
public Button clearButton;
|
|
public RectTransform list;
|
|
public Scrollbar listScrollBar;
|
|
|
|
public Text logText;
|
|
public Scrollbar logScrollBar;
|
|
|
|
readonly Color passColor = new Color(0f, 1f, 0f, 1f); // green
|
|
readonly Color failColor = new Color(1f, 0f, 0f, 1f); // red
|
|
readonly Color normalColor = new Color(1f, 1f, 1f, 1f); // white
|
|
|
|
bool allTestGreen = true;
|
|
|
|
void Start()
|
|
{
|
|
try
|
|
{
|
|
UnityEngine.Application.logMessageReceived += (a, b, c) =>
|
|
{
|
|
logText.text += "[" + c + "]" + a + "\n";
|
|
};
|
|
|
|
// register all test types
|
|
foreach (var item in GetTestTargetTypes())
|
|
{
|
|
RegisterAllMethods(item);
|
|
}
|
|
|
|
var executeAll = new List<Func<Coroutine>>();
|
|
foreach (var ___item in tests)
|
|
{
|
|
var actionList = ___item; // be careful, capture in lambda
|
|
|
|
executeAll.Add(() => StartCoroutine(RunTestInCoroutine(actionList)));
|
|
Add(actionList.Key, () => StartCoroutine(RunTestInCoroutine(actionList)));
|
|
}
|
|
|
|
var executeAllButton = Add("Run All Tests", () => StartCoroutine(ExecuteAllInCoroutine(executeAll)));
|
|
|
|
clearButton.gameObject.GetComponent<Image>().color = new Color(170 / 255f, 170 / 255f, 170 / 255f, 1);
|
|
executeAllButton.gameObject.GetComponent<Image>().color = new Color(250 / 255f, 150 / 255f, 150 / 255f, 1);
|
|
executeAllButton.transform.SetSiblingIndex(1);
|
|
|
|
additionalActionsOnFirst.Reverse();
|
|
foreach (var item in additionalActionsOnFirst)
|
|
{
|
|
var newButton = GameObject.Instantiate(clearButton);
|
|
newButton.name = item.Name;
|
|
newButton.onClick.RemoveAllListeners();
|
|
newButton.GetComponentInChildren<Text>().text = item.Name;
|
|
newButton.onClick.AddListener(item.Action);
|
|
newButton.transform.SetParent(list);
|
|
newButton.transform.SetSiblingIndex(1);
|
|
}
|
|
|
|
clearButton.onClick.AddListener(() =>
|
|
{
|
|
logText.text = "";
|
|
foreach (var btn in list.GetComponentsInChildren<Button>())
|
|
{
|
|
btn.interactable = true;
|
|
btn.GetComponent<Image>().color = normalColor;
|
|
}
|
|
executeAllButton.gameObject.GetComponent<Image>().color = new Color(250 / 255f, 150 / 255f, 150 / 255f, 1);
|
|
});
|
|
|
|
listScrollBar.value = 1;
|
|
logScrollBar.value = 1;
|
|
|
|
if (Application.isBatchMode)
|
|
{
|
|
// run immediately in player
|
|
StartCoroutine(ExecuteAllInCoroutine(executeAll));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (Application.isBatchMode)
|
|
{
|
|
// when failed(can not start runner), quit immediately.
|
|
WriteToConsole(ex.ToString());
|
|
Application.Quit(1);
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
Button Add(string title, UnityAction test)
|
|
{
|
|
var newButton = GameObject.Instantiate(clearButton);
|
|
newButton.name = title;
|
|
newButton.onClick.RemoveAllListeners();
|
|
newButton.GetComponentInChildren<Text>().text = title;
|
|
newButton.onClick.AddListener(test);
|
|
|
|
newButton.transform.SetParent(list);
|
|
return newButton;
|
|
}
|
|
|
|
static IEnumerable<Type> GetTestTargetTypes()
|
|
{
|
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
var n = assembly.FullName;
|
|
if (n.StartsWith("UnityEngine")) continue;
|
|
if (n.StartsWith("mscorlib")) continue;
|
|
if (n.StartsWith("System")) continue;
|
|
|
|
foreach (var item in assembly.GetTypes())
|
|
{
|
|
foreach (var method in item.GetMethods())
|
|
{
|
|
var t1 = method.GetCustomAttribute<TestAttribute>(true);
|
|
if (t1 != null)
|
|
{
|
|
yield return item;
|
|
break;
|
|
}
|
|
var t2 = method.GetCustomAttribute<UnityTestAttribute>(true);
|
|
if (t2 != null)
|
|
{
|
|
yield return item;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void AddTest(string group, string title, Action test)
|
|
{
|
|
List<KeyValuePair<string, object>> list;
|
|
if (!tests.TryGetValue(group, out list))
|
|
{
|
|
list = new List<KeyValuePair<string, object>>();
|
|
tests[group] = list;
|
|
}
|
|
|
|
list.Add(new KeyValuePair<string, object>(title, test));
|
|
}
|
|
|
|
public void AddAsyncTest(string group, string title, Func<IEnumerator> asyncTestCoroutine)
|
|
{
|
|
List<KeyValuePair<string, object>> list;
|
|
if (!tests.TryGetValue(group, out list))
|
|
{
|
|
list = new List<KeyValuePair<string, object>>();
|
|
tests[group] = list;
|
|
}
|
|
|
|
list.Add(new KeyValuePair<string, object>(title, asyncTestCoroutine));
|
|
}
|
|
|
|
public void AddCutomAction(string name, UnityAction action)
|
|
{
|
|
additionalActionsOnFirst.Add(new Pair { Name = name, Action = action });
|
|
}
|
|
|
|
|
|
public void RegisterAllMethods<T>()
|
|
where T : new()
|
|
{
|
|
RegisterAllMethods(typeof(T));
|
|
}
|
|
|
|
public void RegisterAllMethods(Type testType)
|
|
{
|
|
try
|
|
{
|
|
var test = Activator.CreateInstance(testType);
|
|
|
|
var methods = testType.GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
|
foreach (var item in methods)
|
|
{
|
|
try
|
|
{
|
|
var iteratorTest = item.GetCustomAttribute<UnityEngine.TestTools.UnityTestAttribute>(true);
|
|
if (iteratorTest != null)
|
|
{
|
|
if (item.GetParameters().Length == 0 && item.ReturnType == typeof(IEnumerator))
|
|
{
|
|
var factory = (Func<IEnumerator>)Delegate.CreateDelegate(typeof(Func<IEnumerator>), test, item);
|
|
AddAsyncTest(factory.Target.GetType().Name, factory.Method.Name, factory);
|
|
}
|
|
else
|
|
{
|
|
UnityEngine.Debug.Log(testType.Name + "." + item.Name + " currently does not supported in RuntumeUnitTestToolkit(multiple parameter or return type is invalid).");
|
|
}
|
|
}
|
|
|
|
var standardTest = item.GetCustomAttribute<NUnit.Framework.TestAttribute>(true);
|
|
if (standardTest != null)
|
|
{
|
|
if (item.GetParameters().Length == 0 && item.ReturnType == typeof(void))
|
|
{
|
|
var invoke = (Action)Delegate.CreateDelegate(typeof(Action), test, item);
|
|
AddTest(invoke.Target.GetType().Name, invoke.Method.Name, invoke);
|
|
}
|
|
else
|
|
{
|
|
UnityEngine.Debug.Log(testType.Name + "." + item.Name + " currently does not supported in RuntumeUnitTestToolkit(multiple parameter or return type is invalid).");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
UnityEngine.Debug.LogError(testType.Name + "." + item.Name + " failed to register method, exception: " + e.ToString());
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogException(ex);
|
|
}
|
|
}
|
|
|
|
System.Collections.IEnumerator ScrollLogToEndNextFrame()
|
|
{
|
|
yield return null;
|
|
yield return null;
|
|
logScrollBar.value = 0;
|
|
}
|
|
|
|
IEnumerator RunTestInCoroutine(KeyValuePair<string, List<KeyValuePair<string, object>>> actionList)
|
|
{
|
|
Button self = null;
|
|
foreach (var btn in list.GetComponentsInChildren<Button>())
|
|
{
|
|
btn.interactable = false;
|
|
if (btn.name == actionList.Key) self = btn;
|
|
}
|
|
if (self != null)
|
|
{
|
|
self.GetComponent<Image>().color = normalColor;
|
|
}
|
|
|
|
var allGreen = true;
|
|
|
|
logText.text += "<color=yellow>" + actionList.Key + "</color>\n";
|
|
WriteToConsole("Begin Test Class: " + actionList.Key);
|
|
yield return null;
|
|
|
|
var totalExecutionTime = new List<double>();
|
|
foreach (var item2 in actionList.Value)
|
|
{
|
|
// before start, cleanup
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect();
|
|
|
|
logText.text += "<color=teal>" + item2.Key + "</color>\n";
|
|
yield return null;
|
|
|
|
var v = item2.Value;
|
|
|
|
var methodStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
Exception exception = null;
|
|
if (v is Action)
|
|
{
|
|
try
|
|
{
|
|
((Action)v).Invoke();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
exception = ex;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var coroutineFactory = (Func<IEnumerator>)v;
|
|
IEnumerator coroutine = null;
|
|
try
|
|
{
|
|
coroutine = coroutineFactory();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
exception = ex;
|
|
}
|
|
if (exception == null)
|
|
{
|
|
yield return StartCoroutine(UnwrapEnumerator(coroutine, ex =>
|
|
{
|
|
exception = ex;
|
|
}));
|
|
}
|
|
}
|
|
|
|
methodStopwatch.Stop();
|
|
totalExecutionTime.Add(methodStopwatch.Elapsed.TotalMilliseconds);
|
|
if (exception == null)
|
|
{
|
|
logText.text += "OK, " + methodStopwatch.Elapsed.TotalMilliseconds.ToString("0.00") + "ms\n";
|
|
WriteToConsoleResult(item2.Key + ", " + methodStopwatch.Elapsed.TotalMilliseconds.ToString("0.00") + "ms", true);
|
|
}
|
|
else
|
|
{
|
|
// found match line...
|
|
var line = string.Join("\n", exception.StackTrace.Split('\n').Where(x => x.Contains(actionList.Key) || x.Contains(item2.Key)).ToArray());
|
|
logText.text += "<color=red>" + exception.Message + "\n" + line + "</color>\n";
|
|
WriteToConsoleResult(item2.Key + ", " + exception.Message, false);
|
|
WriteToConsole(line);
|
|
allGreen = false;
|
|
allTestGreen = false;
|
|
}
|
|
}
|
|
|
|
logText.text += "[" + actionList.Key + "]" + totalExecutionTime.Sum().ToString("0.00") + "ms\n\n";
|
|
foreach (var btn in list.GetComponentsInChildren<Button>()) btn.interactable = true;
|
|
if (self != null)
|
|
{
|
|
self.GetComponent<Image>().color = allGreen ? passColor : failColor;
|
|
}
|
|
|
|
yield return StartCoroutine(ScrollLogToEndNextFrame());
|
|
|
|
|
|
}
|
|
|
|
IEnumerator ExecuteAllInCoroutine(List<Func<Coroutine>> tests)
|
|
{
|
|
allTestGreen = true;
|
|
|
|
foreach (var item in tests)
|
|
{
|
|
yield return item();
|
|
}
|
|
|
|
if (Application.isBatchMode)
|
|
{
|
|
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
|
|
bool disableAutoClose = (scene.name.Contains("DisableAutoClose"));
|
|
|
|
if (allTestGreen)
|
|
{
|
|
WriteToConsole("Test Complete Successfully");
|
|
if (!disableAutoClose)
|
|
{
|
|
Application.Quit();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WriteToConsole("Test Failed, please see [NG] log.");
|
|
if (!disableAutoClose)
|
|
{
|
|
Application.Quit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
IEnumerator UnwrapEnumerator(IEnumerator enumerator, Action<Exception> exceptionCallback)
|
|
{
|
|
var hasNext = true;
|
|
while (hasNext)
|
|
{
|
|
try
|
|
{
|
|
hasNext = enumerator.MoveNext();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
exceptionCallback(ex);
|
|
hasNext = false;
|
|
}
|
|
|
|
if (hasNext)
|
|
{
|
|
// unwrap self for bug of Unity
|
|
// https://issuetracker.unity3d.com/issues/does-not-stop-coroutine-when-it-throws-exception-in-movenext-at-first-frame
|
|
var moreCoroutine = enumerator.Current as IEnumerator;
|
|
if (moreCoroutine != null)
|
|
{
|
|
yield return StartCoroutine(UnwrapEnumerator(moreCoroutine, ex =>
|
|
{
|
|
exceptionCallback(ex);
|
|
hasNext = false;
|
|
}));
|
|
}
|
|
else
|
|
{
|
|
yield return enumerator.Current;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void WriteToConsole(string msg)
|
|
{
|
|
if (Application.isBatchMode)
|
|
{
|
|
Console.WriteLine(msg);
|
|
}
|
|
}
|
|
|
|
static void WriteToConsoleResult(string msg, bool green)
|
|
{
|
|
if (Application.isBatchMode)
|
|
{
|
|
if (!green)
|
|
{
|
|
var currentForeground = Console.ForegroundColor;
|
|
Console.ForegroundColor = ConsoleColor.Red;
|
|
Console.Write("[NG]");
|
|
Console.ForegroundColor = currentForeground;
|
|
}
|
|
else
|
|
{
|
|
var currentForeground = Console.ForegroundColor;
|
|
Console.ForegroundColor = ConsoleColor.Green;
|
|
Console.Write("[OK]");
|
|
Console.ForegroundColor = currentForeground;
|
|
}
|
|
|
|
System.Console.WriteLine(msg);
|
|
}
|
|
}
|
|
|
|
struct Pair
|
|
{
|
|
public string Name;
|
|
public UnityAction Action;
|
|
}
|
|
}
|
|
}
|