using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Linq; // welcome to the dark side
using Svelto.Tasks;
using Svelto.Tasks.Lean;
using Svelto.Tasks.Enumerators;
using UnityEngine;
using TechbloxModdingAPI.App;
using TechbloxModdingAPI.Tasks;
using TechbloxModdingAPI.Utility;
namespace TechbloxModdingAPI.Tests
{
///
/// API test system root class.
///
public static class TestRoot
{
public static bool AutoShutdown = true;
public const string ReportFile = "TechbloxModdingAPI_tests.log";
private static bool _testsPassed = false;
private static uint _testsCount = 0;
private static uint _testsCountPassed = 0;
private static uint _testsCountFailed = 0;
private static string state = "StartingUp";
private static Stopwatch timer;
private static List testTypes = null;
public static bool TestsPassed
{
get => _testsPassed;
set
{
_testsPassed = _testsPassed && value;
_testsCount++;
if (value)
{
_testsCountPassed++;
}
else
{
_testsCountFailed++;
}
}
}
private static void StartUp()
{
// init
timer = Stopwatch.StartNew();
_testsPassed = true;
_testsCount = 0;
_testsCountPassed = 0;
_testsCountFailed = 0;
// flow control
Game.Enter += (sender, args) => { GameTests().RunOn(RobocraftX.Schedulers.ClientLean.EveryFrameStepRunner_TimeRunningAndStopped); };
Game.Exit += (s, a) => state = "ReturningFromGame";
Client.EnterMenu += (sender, args) =>
{
if (state == "EnteringMenu")
{
MenuTests().RunOn(Scheduler.leanRunner);
state = "EnteringGame";
}
if (state == "ReturningFromGame")
{
TearDown().RunOn(Scheduler.leanRunner);
state = "ShuttingDown";
}
};
// init tests here
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
if (m.GetCustomAttribute() != null)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Start up method '{m}' raised an exception: {e.ToString()}");
}
}
}
}
state = "EnteringMenu";
}
private static IEnumerator MenuTests()
{
yield return Yield.It;
// menu tests
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute();
if (a != null && a.TestType == TestType.Menu)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"Menu test '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
// load game
yield return GoToGameTests().Continue();
}
private static IEnumerator GoToGameTests()
{
Client app = Client.Instance;
int oldLength = 0;
while (app.MyGames.Length == 0 || oldLength != app.MyGames.Length)
{
oldLength = app.MyGames.Length;
yield return new WaitForSecondsEnumerator(1).Continue();
}
yield return Yield.It;
try
{
app.MyGames[0].EnterGame();
}
catch (Exception e)
{
Console.WriteLine("Failed to go to game tests");
Console.WriteLine(e);
}
/*Game newGame = Game.NewGame();
yield return new WaitForSecondsEnumerator(5).Continue(); // wait for sync
newGame.EnterGame();*/
}
private static IEnumerator GameTests()
{
yield return Yield.It;
Game currentGame = Game.CurrentGame();
// in-game tests
yield return new WaitForSecondsEnumerator(5).Continue(); // wait for game to finish loading
var testTypesToRun = new[]
{
TestType.Game,
TestType.SimulationMode,
TestType.EditMode
};
for (var index = 0; index < testTypesToRun.Length; index++)
{
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
APITestCaseAttribute a = m.GetCustomAttribute();
if (a == null || a.TestType != testTypesToRun[index]) continue;
object ret = null;
try
{
ret = m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Fail($"{a.TestType} test '{m}' raised an exception: {e}");
}
if (ret is IEnumerator enumerator)
{ //Support enumerator methods with added exception handling
bool cont;
do
{ //Can't use yield return in a try block...
try
{ //And with Continue() exceptions aren't caught
cont = enumerator.MoveNext();
}
catch (Exception e)
{
Assert.Fail($"{a.TestType} test '{m}' raised an exception: {e}");
cont = false;
}
yield return Yield.It;
} while (cont);
}
yield return Yield.It;
}
}
if (index + 1 < testTypesToRun.Length) //Don't toggle on the last test
currentGame.ToggleTimeMode();
yield return new WaitForSecondsEnumerator(5).Continue();
}
// exit game
yield return ReturnToMenu().Continue();
}
private static IEnumerator ReturnToMenu()
{
Logging.MetaLog("Returning to main menu");
yield return Yield.It;
Game.CurrentGame().ExitGame();
}
private static IEnumerator TearDown()
{
yield return new WaitForSecondsEnumerator(5).Continue();
Logging.MetaLog("Tearing down test run");
// dispose tests here
foreach (Type t in testTypes)
{
foreach (MethodBase m in t.GetMethods())
{
if (m.GetCustomAttribute() != null)
{
try
{
m.Invoke(null, new object[0]);
}
catch (Exception e)
{
Assert.Warn($"Tear down method '{m}' raised an exception: {e.ToString()}");
}
yield return Yield.It;
}
}
}
// finish up
Assert.CallsComplete();
timer.Stop();
string verdict = _testsPassed ? "--- PASSED :) ---" : "--- FAILED :( ---";
Assert.Log($"VERDICT: {verdict} ({_testsCountPassed}/{_testsCountFailed}/{_testsCount} P/F/T in {timer.ElapsedMilliseconds}ms)");
yield return Yield.It;
// end game
Logging.MetaLog("Completed test run: " + verdict);
yield return Yield.It;
Assert.CloseLog();
if (AutoShutdown) Application.Quit();
}
private static void FindTests(Assembly asm)
{
testTypes = new List();
foreach (Type t in asm.GetTypes())
{
if (t.GetCustomAttribute() != null)
{
testTypes.Add(t);
}
}
}
///
/// Runs the tests.
///
/// Assembly to search for tests. When set to null, this uses the TechbloxModdingAPI assembly.
public static void RunTests(Assembly asm = null)
{
if (asm == null) asm = Assembly.GetExecutingAssembly();
FindTests(asm);
Logging.MetaLog("Starting test run");
// log metadata
Assert.Log($"Unity {Application.unityVersion}");
Assert.Log($"Techblox {Application.version}");
Assert.Log($"TechbloxModdingAPI {Assembly.GetExecutingAssembly().GetName().Version}");
Assert.Log($"Testing {asm.GetName().Name} {asm.GetName().Version}");
Assert.Log($"START: --- {DateTime.Now.ToString()} --- ({testTypes.Count} tests classes detected)");
StartUp();
Logging.MetaLog("Test StartUp complete");
}
}
}