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"); } } }