A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using UnityEditor.Build.Reporting;
6using UnityEditor.SceneManagement;
7using UnityEditor.TestRunner.TestLaunchers;
8using UnityEditor.TestTools.TestRunner.Api;
9using UnityEngine;
10using UnityEngine.SceneManagement;
11using UnityEngine.TestRunner.Utils;
12using UnityEngine.TestTools.TestRunner;
13using UnityEngine.TestTools.TestRunner.Callbacks;
14using Object = UnityEngine.Object;
15
16namespace UnityEditor.TestTools.TestRunner
17{
18 internal class TestLaunchFailedException : Exception
19 {
20 public TestLaunchFailedException() {}
21 public TestLaunchFailedException(string message) : base(message) {}
22 }
23
24 [Serializable]
25 internal class PlayerLauncher : RuntimeTestLauncherBase
26 {
27 private readonly BuildTarget m_TargetPlatform;
28 private ITestRunSettings m_OverloadTestRunSettings;
29 private string m_SceneName;
30 private Scene m_Scene;
31 private int m_HeartbeatTimeout;
32 private string m_PlayerWithTestsPath;
33 private PlaymodeTestsController m_Runner;
34
35 internal PlayerLauncherBuildOptions playerBuildOptions { get; private set; }
36
37 public PlayerLauncher(PlaymodeTestsControllerSettings settings, BuildTarget? targetPlatform, ITestRunSettings overloadTestRunSettings, int heartbeatTimeout, string playerWithTestsPath, string scenePath, Scene scene, PlaymodeTestsController runner) : base(settings)
38 {
39 m_TargetPlatform = targetPlatform ?? EditorUserBuildSettings.activeBuildTarget;
40 m_OverloadTestRunSettings = overloadTestRunSettings;
41 m_HeartbeatTimeout = heartbeatTimeout;
42 m_PlayerWithTestsPath = playerWithTestsPath;
43 m_SceneName = scenePath;
44 m_Scene = scene;
45 m_Runner = runner;
46 }
47
48 protected override RuntimePlatform? TestTargetPlatform
49 {
50 get { return BuildTargetConverter.TryConvertToRuntimePlatform(m_TargetPlatform); }
51 }
52
53 public override void Run()
54 {
55 var editorConnectionTestCollector = RemoteTestRunController.instance;
56 editorConnectionTestCollector.hideFlags = HideFlags.HideAndDontSave;
57 editorConnectionTestCollector.Init(m_TargetPlatform, m_HeartbeatTimeout);
58
59 var remotePlayerLogController = RemotePlayerLogController.instance;
60 remotePlayerLogController.hideFlags = HideFlags.HideAndDontSave;
61
62 using (var settings = new PlayerLauncherContextSettings(m_OverloadTestRunSettings))
63 {
64 PrepareScene(m_SceneName, m_Scene, m_Runner);
65
66 var filter = m_Settings.BuildNUnitFilter();
67 var runner = LoadTests(filter);
68 var exceptionThrown = ExecutePreBuildSetupMethods(runner.LoadedTest, filter);
69 if (exceptionThrown)
70 {
71 ReopenOriginalScene(m_Settings.originalScene);
72 CallbacksDelegator.instance.RunFailed("Run Failed: One or more errors in a prebuild setup. See the editor log for details.");
73 return;
74 }
75
76 EditorSceneManager.MarkSceneDirty(m_Scene);
77 EditorSceneManager.SaveScene(m_Scene);
78
79 playerBuildOptions = GetBuildOptions(m_SceneName);
80
81 var success = BuildAndRunPlayer(playerBuildOptions);
82
83 FilePathMetaInfo.TryCreateFile(runner.LoadedTest, playerBuildOptions.BuildPlayerOptions);
84 ExecutePostBuildCleanupMethods(runner.LoadedTest, filter);
85
86 ReopenOriginalScene(m_Settings.originalScene);
87
88 if (!success)
89 {
90 Object.DestroyImmediate(editorConnectionTestCollector);
91 Debug.LogError("Player build failed");
92 throw new TestLaunchFailedException("Player build failed");
93 }
94
95 if ((playerBuildOptions.BuildPlayerOptions.options & BuildOptions.AutoRunPlayer) != 0)
96 {
97 editorConnectionTestCollector.PostSuccessfulBuildAction();
98 }
99
100 var runSettings = m_OverloadTestRunSettings as PlayerLauncherTestRunSettings;
101 if (success && runSettings != null && runSettings.buildOnly)
102 {
103 EditorUtility.RevealInFinder(playerBuildOptions.BuildPlayerOptions.locationPathName);
104 }
105 }
106 }
107
108 public void PrepareScene(string sceneName, Scene scene, PlaymodeTestsController runner)
109 {
110 runner.AddEventHandlerMonoBehaviour<PlayModeRunnerCallback>();
111 var commandLineArgs = Environment.GetCommandLineArgs();
112 if (!commandLineArgs.Contains("-doNotReportTestResultsBackToEditor"))
113 {
114 runner.AddEventHandlerMonoBehaviour<RemoteTestResultSender>();
115 }
116 runner.AddEventHandlerMonoBehaviour<PlayerQuitHandler>();
117 runner.AddEventHandlerScriptableObject<TestRunCallbackListener>();
118
119 EditorSceneManager.MarkSceneDirty(scene);
120 AssetDatabase.SaveAssets();
121 EditorSceneManager.SaveScene(scene, sceneName, false);
122 }
123
124 private static bool BuildAndRunPlayer(PlayerLauncherBuildOptions buildOptions)
125 {
126 Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "Building player with following options:\n{0}", buildOptions);
127
128#if !UNITY_2021_2_OR_NEWER
129 // Android has to be in listen mode to establish player connection
130 // Only flip connect to host if we are older than Unity 2021.2
131 if (buildOptions.BuildPlayerOptions.target == BuildTarget.Android)
132 {
133 buildOptions.BuildPlayerOptions.options &= ~BuildOptions.ConnectToHost;
134 }
135#endif
136 // For now, so does Lumin
137#if !UNITY_2022_2_OR_NEWER
138 if (buildOptions.BuildPlayerOptions.target == BuildTarget.Lumin)
139 {
140 buildOptions.BuildPlayerOptions.options &= ~BuildOptions.ConnectToHost;
141 }
142#endif
143
144#if UNITY_2023_2_OR_NEWER
145 // WebGL has to be in close on quit mode to ensure that the browser tab is closed when the player finishes running tests
146 if (buildOptions.BuildPlayerOptions.target == BuildTarget.WebGL)
147 {
148 PlayerSettings.WebGL.closeOnQuit = true;
149 }
150#endif
151
152 var result = BuildPipeline.BuildPlayer(buildOptions.BuildPlayerOptions);
153 if (result.summary.result != BuildResult.Succeeded)
154 Debug.LogError(result.SummarizeErrors());
155
156#if UNITY_2023_2_OR_NEWER
157 // Clean up WebGL close on quit mode
158 if (buildOptions.BuildPlayerOptions.target == BuildTarget.WebGL)
159 {
160 PlayerSettings.WebGL.closeOnQuit = false;
161 }
162#endif
163
164 return result.summary.result == BuildResult.Succeeded;
165 }
166
167 internal PlayerLauncherBuildOptions GetBuildOptions(string scenePath)
168 {
169 var buildOnly = false;
170 var runSettings = m_OverloadTestRunSettings as PlayerLauncherTestRunSettings;
171 if (runSettings != null)
172 {
173 buildOnly = runSettings.buildOnly;
174 }
175
176 var buildOptions = new BuildPlayerOptions();
177
178 var scenes = new List<string> { scenePath };
179 scenes.AddRange(EditorBuildSettings.scenes.Select(x => x.path));
180 buildOptions.scenes = scenes.ToArray();
181
182 buildOptions.options |= BuildOptions.Development | BuildOptions.ConnectToHost | BuildOptions.IncludeTestAssemblies | BuildOptions.StrictMode;
183 buildOptions.target = m_TargetPlatform;
184
185#if UNITY_2021_2_OR_NEWER
186 buildOptions.subtarget = EditorUserBuildSettings.GetActiveSubtargetFor(m_TargetPlatform);
187#endif
188
189 if (EditorUserBuildSettings.waitForPlayerConnection)
190 buildOptions.options |= BuildOptions.WaitForPlayerConnection;
191
192 if (EditorUserBuildSettings.allowDebugging)
193 buildOptions.options |= BuildOptions.AllowDebugging;
194
195 if (EditorUserBuildSettings.installInBuildFolder)
196 buildOptions.options |= BuildOptions.InstallInBuildFolder;
197 else if (!buildOnly)
198 buildOptions.options |= BuildOptions.AutoRunPlayer;
199
200 var buildTargetGroup = EditorUserBuildSettings.activeBuildTargetGroup;
201 buildOptions.targetGroup = buildTargetGroup;
202
203 //Check if Lz4 is supported for the current buildtargetgroup and enable it if need be
204 if (PostprocessBuildPlayer.SupportsLz4Compression(buildTargetGroup, m_TargetPlatform))
205 {
206 if (EditorUserBuildSettings.GetCompressionType(buildTargetGroup) == Compression.Lz4)
207 buildOptions.options |= BuildOptions.CompressWithLz4;
208 else if (EditorUserBuildSettings.GetCompressionType(buildTargetGroup) == Compression.Lz4HC)
209 buildOptions.options |= BuildOptions.CompressWithLz4HC;
210 }
211
212 string buildLocation;
213 if (buildOnly)
214 {
215 buildLocation = buildOptions.locationPathName = runSettings.buildOnlyLocationPath;
216 }
217 else
218 {
219 var reduceBuildLocationPathLength = false;
220
221 //Some platforms hit MAX_PATH limits during the build process, in these cases minimize the path length
222 if ((m_TargetPlatform == BuildTarget.WSAPlayer)
223#if !UNITY_2021_1_OR_NEWER
224 || (m_TargetPlatform == BuildTarget.XboxOne)
225#endif
226 )
227 {
228 reduceBuildLocationPathLength = true;
229 }
230
231 var uniqueTempPathInProject = FileUtil.GetUniqueTempPathInProject();
232 var playerDirectoryName = "PlayerWithTests";
233
234 //Some platforms hit MAX_PATH limits during the build process, in these cases minimize the path length
235 if (reduceBuildLocationPathLength)
236 {
237 playerDirectoryName = "PwT";
238 uniqueTempPathInProject = Path.GetTempFileName();
239 File.Delete(uniqueTempPathInProject);
240 Directory.CreateDirectory(uniqueTempPathInProject);
241 }
242
243 buildLocation = Path.Combine(string.IsNullOrEmpty(m_PlayerWithTestsPath) ? Path.GetFullPath(uniqueTempPathInProject) : m_PlayerWithTestsPath, playerDirectoryName);
244
245 // iOS builds create a folder with Xcode project instead of an executable, therefore no executable name is added
246 if (m_TargetPlatform == BuildTarget.iOS)
247 {
248 buildOptions.locationPathName = buildLocation;
249 }
250 else
251 {
252 string extensionForBuildTarget =
253 PostprocessBuildPlayer.GetExtensionForBuildTarget(buildTargetGroup, buildOptions.target,
254 buildOptions.options);
255 var playerExecutableName = "PlayerWithTests";
256 if (!string.IsNullOrEmpty(extensionForBuildTarget))
257 playerExecutableName += $".{extensionForBuildTarget}";
258
259 buildOptions.locationPathName = Path.Combine(buildLocation, playerExecutableName);
260 }
261 }
262
263 return new PlayerLauncherBuildOptions
264 {
265 BuildPlayerOptions = ModifyBuildOptions(buildOptions),
266 PlayerDirectory = buildLocation,
267 };
268 }
269
270 private BuildPlayerOptions ModifyBuildOptions(BuildPlayerOptions buildOptions)
271 {
272 var allAssemblies = AppDomain.CurrentDomain.GetAssemblies()
273 .Where(x => x.GetReferencedAssemblies().Any(z => z.Name == "UnityEditor.TestRunner")).ToArray();
274 var attributes = allAssemblies.SelectMany(assembly => assembly.GetCustomAttributes(typeof(TestPlayerBuildModifierAttribute), true).OfType<TestPlayerBuildModifierAttribute>()).ToArray();
275 var modifiers = attributes.Select(attribute => attribute.ConstructModifier()).ToArray();
276
277 foreach (var modifier in modifiers)
278 {
279 buildOptions = modifier.ModifyOptions(buildOptions);
280 }
281
282 return buildOptions;
283 }
284
285 private static bool ShouldReduceBuildLocationPathLength(BuildTarget target)
286 {
287 switch (target)
288 {
289#if UNITY_2020_2_OR_NEWER
290 case BuildTarget.GameCoreXboxOne:
291 case BuildTarget.GameCoreXboxSeries:
292#else
293 case BuildTarget.XboxOne:
294#endif
295 case BuildTarget.WSAPlayer:
296 return true;
297 default:
298 return false;
299 }
300 }
301 }
302}