A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Linq;
5using Unity.Profiling;
6using UnityEditor.TestTools.TestRunner.Api;
7using UnityEditor.TestTools.TestRunner.TestRun.Tasks;
8using UnityEngine;
9using UnityEngine.TestTools;
10
11namespace UnityEditor.TestTools.TestRunner.TestRun
12{
13 internal class TestJobRunner : ITestJobRunner
14 {
15 internal ITestJobDataHolder testJobDataHolder = TestJobDataHolder.instance;
16
17 internal Action<EditorApplication.CallbackFunction> SubscribeCallback =
18 callback => EditorApplication.update += callback;
19
20 // ReSharper disable once DelegateSubtraction
21 internal Action<EditorApplication.CallbackFunction> UnsubscribeCallback =
22 callback => EditorApplication.update -= callback;
23
24 internal TestCommandPcHelper PcHelper = new EditModePcHelper();
25 internal Func<ExecutionSettings, IEnumerable<TestTaskBase>> GetTasks = TaskList.GetTaskList;
26 internal Action<Exception> LogException = Debug.LogException;
27 internal Action<string> LogError = Debug.LogError;
28 internal Action<string> ReportRunFailed = CallbacksDelegator.instance.RunFailed;
29 internal Func<TestRunnerApi.RunProgressChangedEvent> RunProgressChanged = () => TestRunnerApi.runProgressChanged;
30
31 private TestJobData m_JobData;
32 private IEnumerator m_Enumerator;
33 private string m_CurrentTaskName;
34
35 public string RunJob(TestJobData data)
36 {
37 if (data == null)
38 {
39 throw new ArgumentException(null, nameof(data));
40 }
41
42 if (data.taskInfoStack == null)
43 {
44 throw new ArgumentException($"{nameof(data.taskInfoStack)} on {nameof(TestJobData)} is null.",
45 nameof(data));
46 }
47
48 if (IsRunningJob())
49 {
50 throw new Exception("TestJobRunner is already running a job.");
51 }
52
53 if (data.isHandledByRunner)
54 {
55 throw new Exception("Test job is already being handled.");
56 }
57
58 m_JobData = data;
59 m_JobData.isHandledByRunner = true;
60
61 if (!IsRunningJob())
62 {
63 m_JobData.isRunning = true;
64 m_JobData.taskInfoStack.Push(new TaskInfo());
65 testJobDataHolder.RegisterRun(this, m_JobData);
66 }
67 else // Is resuming run
68 {
69 var taskInfoBeforeResuming = m_JobData.taskInfoStack.Peek();
70 if (taskInfoBeforeResuming.taskMode != TaskMode.Resume)
71 {
72 m_JobData.taskInfoStack.Push(new TaskInfo
73 {
74 taskMode = TaskMode.Resume,
75 index = 0,
76 stopBeforeIndex = taskInfoBeforeResuming.index + (taskInfoBeforeResuming.pc > 0 ? 1 : 0)
77 });
78 }
79 else
80 {
81 taskInfoBeforeResuming.index = 0;
82 }
83 }
84
85 m_JobData.Tasks = GetTasks(data.executionSettings).ToArray();
86 if (m_JobData.Tasks.Length == 0)
87 {
88 throw new Exception($"No tasks founds for {data.executionSettings}");
89 }
90
91 if (!data.executionSettings.runSynchronously)
92 {
93 SubscribeCallback(ExecuteCallback);
94 }
95 else
96 {
97 while (data.isRunning)
98 {
99 ExecuteStep();
100 }
101 }
102
103 return data.guid;
104 }
105
106 private void ExecuteCallback()
107 {
108 ExecuteStep();
109 var c = 0;
110 while (ShouldExecuteInstantly())
111 {
112 ExecuteStep();
113 c++;
114
115 if (c > 500)
116 {
117 var taskInfo = m_JobData.taskInfoStack.Peek();
118 var taskName = taskInfo != null ? m_JobData.Tasks[taskInfo.index].GetType().Name : "null";
119 Debug.LogError(
120 $"Too many instant steps in test execution mode: {taskInfo?.taskMode}. Current task {taskName}.");
121 StopRun();
122 return;
123 }
124 }
125 }
126
127 private void ExecuteStep()
128 {
129 using (new ProfilerMarker(nameof(TestJobRunner) + "." + nameof(ExecuteStep)).Auto())
130 {
131 try
132 {
133 if (m_JobData.taskInfoStack.Count == 0)
134 {
135 StopRun();
136 return;
137 }
138
139 var taskInfo = m_JobData.taskInfoStack.Peek();
140
141 if (m_Enumerator == null)
142 {
143 if (taskInfo.index >= m_JobData.Tasks.Length || (taskInfo.stopBeforeIndex > 0 &&
144 taskInfo.index >= taskInfo.stopBeforeIndex))
145 {
146 m_JobData.taskInfoStack.Pop();
147 return;
148 }
149
150 var task = m_JobData.Tasks[taskInfo.index];
151 if (!task.ShouldExecute(taskInfo))
152 {
153 taskInfo.index++;
154 return;
155 }
156
157 m_JobData.runProgress.stepName = task.GetTitle();
158 m_CurrentTaskName = task.GetName();
159 using (new ProfilerMarker(m_CurrentTaskName + ".Setup").Auto())
160 {
161 m_Enumerator = task.Execute(m_JobData);
162 }
163
164 if (task.SupportsResumingEnumerator)
165 {
166 m_Enumerator.MoveNext(); // Execute the first step, to set the job data.
167 PcHelper.SetEnumeratorPC(m_Enumerator, taskInfo.pc);
168 }
169 }
170
171 using (new ProfilerMarker(m_CurrentTaskName + ".Progress").Auto())
172 {
173 var taskIsDone = !m_Enumerator.MoveNext();
174 if (!m_JobData.executionSettings.runSynchronously && taskInfo.taskMode == TaskMode.Normal)
175 {
176 if (taskIsDone)
177 {
178 m_JobData.runProgress.progress += RunProgress.progressPrTask;
179 }
180 ReportRunProgress(false);
181 }
182
183 if (taskIsDone)
184 {
185 taskInfo.index++;
186 taskInfo.pc = 0;
187 m_Enumerator = null;
188
189 return;
190 }
191 }
192
193 if (m_JobData.Tasks[taskInfo.index].SupportsResumingEnumerator)
194 {
195 taskInfo.pc = PcHelper.GetEnumeratorPC(m_Enumerator);
196 }
197 }
198 catch (TestRunCanceledException)
199 {
200 StopRun();
201 }
202 catch (AggregateException ex)
203 {
204 MarkJobAsError();
205 LogError(ex.Message);
206 foreach (var innerException in ex.InnerExceptions)
207 {
208 LogException(innerException);
209 }
210
211 ReportRunFailed("Multiple unexpected errors happened while running tests.");
212 }
213 catch (Exception ex)
214 {
215 MarkJobAsError();
216 LogException(ex);
217 ReportRunFailed("An unexpected error happened while running tests.");
218 }
219 }
220 }
221
222 public bool CancelRun()
223 {
224 if (m_JobData == null || m_JobData.taskInfoStack.Count == 0 ||
225 m_JobData.taskInfoStack.Peek().taskMode == TaskMode.Canceled)
226 {
227 return false;
228 }
229
230 var lastIndex = m_JobData.taskInfoStack.Last().index;
231 m_JobData.taskInfoStack.Clear();
232 m_JobData.taskInfoStack.Push(new TaskInfo
233 {
234 index = lastIndex,
235 taskMode = TaskMode.Canceled
236 });
237 m_Enumerator = null;
238 return true;
239 }
240
241 private bool ShouldExecuteInstantly()
242 {
243 if (m_JobData.taskInfoStack.Count == 0)
244 {
245 return false;
246 }
247
248 var taskInfo = m_JobData.taskInfoStack.Peek();
249 var canRunInstantly =
250 m_JobData.Tasks.Length <= taskInfo.index || m_JobData.Tasks[taskInfo.index].CanRunInstantly;
251 return taskInfo.taskMode != TaskMode.Normal && taskInfo.taskMode != TaskMode.Canceled && canRunInstantly;
252 }
253
254 public bool IsRunningJob()
255 {
256 return m_JobData != null && m_JobData.taskInfoStack != null && m_JobData.taskInfoStack.Count > 0;
257 }
258
259 public TestJobData GetData()
260 {
261 return m_JobData;
262 }
263
264 private void StopRun()
265 {
266 m_JobData.isRunning = false;
267 UnsubscribeCallback(ExecuteCallback);
268 testJobDataHolder.UnregisterRun(this, m_JobData);
269
270 foreach (var task in m_JobData.Tasks)
271 {
272 if (task is IDisposable disposableTask)
273 {
274 try
275 {
276 disposableTask.Dispose();
277 }
278 catch (Exception e)
279 {
280 Debug.LogException(e);
281 }
282 }
283 }
284
285 if (!m_JobData.executionSettings.runSynchronously)
286 {
287 ReportRunProgress(true);
288 }
289 }
290
291 private void ReportRunProgress(bool runHasFinished)
292 {
293 RunProgressChanged().Invoke(new TestRunProgress
294 {
295 CurrentStageName = m_JobData.runProgress.stageName ?? "",
296 CurrentStepName = m_JobData.runProgress.stepName ?? "",
297 Progress = m_JobData.runProgress.progress,
298 ExecutionSettings = m_JobData.executionSettings,
299 RunGuid = m_JobData.guid ?? "",
300 HasFinished = runHasFinished,
301 });
302 }
303
304 private void MarkJobAsError()
305 {
306 var currentTaskInfo = m_JobData.taskInfoStack.Peek();
307 currentTaskInfo.taskMode = TaskMode.Error;
308 currentTaskInfo.index++;
309 currentTaskInfo.pc = 0;
310 m_Enumerator = null;
311 }
312 }
313}