using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.Profiling; using UnityEditor.TestTools.TestRunner.Api; using UnityEditor.TestTools.TestRunner.TestRun.Tasks; using UnityEngine; using UnityEngine.TestTools; namespace UnityEditor.TestTools.TestRunner.TestRun { internal class TestJobRunner : ITestJobRunner { internal ITestJobDataHolder testJobDataHolder = TestJobDataHolder.instance; internal Action SubscribeCallback = callback => EditorApplication.update += callback; // ReSharper disable once DelegateSubtraction internal Action UnsubscribeCallback = callback => EditorApplication.update -= callback; internal TestCommandPcHelper PcHelper = new EditModePcHelper(); internal Func> GetTasks = TaskList.GetTaskList; internal Action LogException = Debug.LogException; internal Action LogError = Debug.LogError; internal Action ReportRunFailed = CallbacksDelegator.instance.RunFailed; internal Func RunProgressChanged = () => TestRunnerApi.runProgressChanged; private TestJobData m_JobData; private IEnumerator m_Enumerator; private string m_CurrentTaskName; public string RunJob(TestJobData data) { if (data == null) { throw new ArgumentException(null, nameof(data)); } if (data.taskInfoStack == null) { throw new ArgumentException($"{nameof(data.taskInfoStack)} on {nameof(TestJobData)} is null.", nameof(data)); } if (IsRunningJob()) { throw new Exception("TestJobRunner is already running a job."); } if (data.isHandledByRunner) { throw new Exception("Test job is already being handled."); } m_JobData = data; m_JobData.isHandledByRunner = true; if (!IsRunningJob()) { m_JobData.isRunning = true; m_JobData.taskInfoStack.Push(new TaskInfo()); testJobDataHolder.RegisterRun(this, m_JobData); } else // Is resuming run { var taskInfoBeforeResuming = m_JobData.taskInfoStack.Peek(); if (taskInfoBeforeResuming.taskMode != TaskMode.Resume) { m_JobData.taskInfoStack.Push(new TaskInfo { taskMode = TaskMode.Resume, index = 0, stopBeforeIndex = taskInfoBeforeResuming.index + (taskInfoBeforeResuming.pc > 0 ? 1 : 0) }); } else { taskInfoBeforeResuming.index = 0; } } m_JobData.Tasks = GetTasks(data.executionSettings).ToArray(); if (m_JobData.Tasks.Length == 0) { throw new Exception($"No tasks founds for {data.executionSettings}"); } if (!data.executionSettings.runSynchronously) { SubscribeCallback(ExecuteCallback); } else { while (data.isRunning) { ExecuteStep(); } } return data.guid; } private void ExecuteCallback() { ExecuteStep(); var c = 0; while (ShouldExecuteInstantly()) { ExecuteStep(); c++; if (c > 500) { var taskInfo = m_JobData.taskInfoStack.Peek(); var taskName = taskInfo != null ? m_JobData.Tasks[taskInfo.index].GetType().Name : "null"; Debug.LogError( $"Too many instant steps in test execution mode: {taskInfo?.taskMode}. Current task {taskName}."); StopRun(); return; } } } private void ExecuteStep() { using (new ProfilerMarker(nameof(TestJobRunner) + "." + nameof(ExecuteStep)).Auto()) { try { if (m_JobData.taskInfoStack.Count == 0) { StopRun(); return; } var taskInfo = m_JobData.taskInfoStack.Peek(); if (m_Enumerator == null) { if (taskInfo.index >= m_JobData.Tasks.Length || (taskInfo.stopBeforeIndex > 0 && taskInfo.index >= taskInfo.stopBeforeIndex)) { m_JobData.taskInfoStack.Pop(); return; } var task = m_JobData.Tasks[taskInfo.index]; if (!task.ShouldExecute(taskInfo)) { taskInfo.index++; return; } m_JobData.runProgress.stepName = task.GetTitle(); m_CurrentTaskName = task.GetName(); using (new ProfilerMarker(m_CurrentTaskName + ".Setup").Auto()) { m_Enumerator = task.Execute(m_JobData); } if (task.SupportsResumingEnumerator) { m_Enumerator.MoveNext(); // Execute the first step, to set the job data. PcHelper.SetEnumeratorPC(m_Enumerator, taskInfo.pc); } } using (new ProfilerMarker(m_CurrentTaskName + ".Progress").Auto()) { var taskIsDone = !m_Enumerator.MoveNext(); if (!m_JobData.executionSettings.runSynchronously && taskInfo.taskMode == TaskMode.Normal) { if (taskIsDone) { m_JobData.runProgress.progress += RunProgress.progressPrTask; } ReportRunProgress(false); } if (taskIsDone) { taskInfo.index++; taskInfo.pc = 0; m_Enumerator = null; return; } } if (m_JobData.Tasks[taskInfo.index].SupportsResumingEnumerator) { taskInfo.pc = PcHelper.GetEnumeratorPC(m_Enumerator); } } catch (TestRunCanceledException) { StopRun(); } catch (AggregateException ex) { MarkJobAsError(); LogError(ex.Message); foreach (var innerException in ex.InnerExceptions) { LogException(innerException); } ReportRunFailed("Multiple unexpected errors happened while running tests."); } catch (Exception ex) { MarkJobAsError(); LogException(ex); ReportRunFailed("An unexpected error happened while running tests."); } } } public bool CancelRun() { if (m_JobData == null || m_JobData.taskInfoStack.Count == 0 || m_JobData.taskInfoStack.Peek().taskMode == TaskMode.Canceled) { return false; } var lastIndex = m_JobData.taskInfoStack.Last().index; m_JobData.taskInfoStack.Clear(); m_JobData.taskInfoStack.Push(new TaskInfo { index = lastIndex, taskMode = TaskMode.Canceled }); m_Enumerator = null; return true; } private bool ShouldExecuteInstantly() { if (m_JobData.taskInfoStack.Count == 0) { return false; } var taskInfo = m_JobData.taskInfoStack.Peek(); var canRunInstantly = m_JobData.Tasks.Length <= taskInfo.index || m_JobData.Tasks[taskInfo.index].CanRunInstantly; return taskInfo.taskMode != TaskMode.Normal && taskInfo.taskMode != TaskMode.Canceled && canRunInstantly; } public bool IsRunningJob() { return m_JobData != null && m_JobData.taskInfoStack != null && m_JobData.taskInfoStack.Count > 0; } public TestJobData GetData() { return m_JobData; } private void StopRun() { m_JobData.isRunning = false; UnsubscribeCallback(ExecuteCallback); testJobDataHolder.UnregisterRun(this, m_JobData); foreach (var task in m_JobData.Tasks) { if (task is IDisposable disposableTask) { try { disposableTask.Dispose(); } catch (Exception e) { Debug.LogException(e); } } } if (!m_JobData.executionSettings.runSynchronously) { ReportRunProgress(true); } } private void ReportRunProgress(bool runHasFinished) { RunProgressChanged().Invoke(new TestRunProgress { CurrentStageName = m_JobData.runProgress.stageName ?? "", CurrentStepName = m_JobData.runProgress.stepName ?? "", Progress = m_JobData.runProgress.progress, ExecutionSettings = m_JobData.executionSettings, RunGuid = m_JobData.guid ?? "", HasFinished = runHasFinished, }); } private void MarkJobAsError() { var currentTaskInfo = m_JobData.taskInfoStack.Peek(); currentTaskInfo.taskMode = TaskMode.Error; currentTaskInfo.index++; currentTaskInfo.pc = 0; m_Enumerator = null; } } }