384 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			384 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
|  | using System; | ||
|  | using System.Collections.Generic; | ||
|  | using System.IO; | ||
|  | using System.Linq; | ||
|  | using UnityEditor.Build.Reporting; | ||
|  | using UnityEditor.SceneManagement; | ||
|  | using UnityEditor.TestRunner.TestLaunchers; | ||
|  | using UnityEditor.TestTools.TestRunner.Api; | ||
|  | using UnityEngine; | ||
|  | using UnityEngine.SceneManagement; | ||
|  | using UnityEngine.TestRunner.Utils; | ||
|  | using UnityEngine.TestTools.TestRunner; | ||
|  | using UnityEngine.TestTools.TestRunner.Callbacks; | ||
|  | using Object = UnityEngine.Object; | ||
|  | 
 | ||
|  | namespace UnityEditor.TestTools.TestRunner | ||
|  | { | ||
|  |     internal class TestLaunchFailedException : Exception | ||
|  |     { | ||
|  |         public TestLaunchFailedException() {} | ||
|  |         public TestLaunchFailedException(string message) : base(message) {} | ||
|  |     } | ||
|  | 
 | ||
|  |     [Serializable] | ||
|  |     internal class PlayerLauncher : RuntimeTestLauncherBase | ||
|  |     { | ||
|  |         private readonly BuildTarget m_TargetPlatform; | ||
|  |         private ITestRunSettings m_OverloadTestRunSettings; | ||
|  |         private string m_SceneName; | ||
|  |         private Scene m_Scene; | ||
|  |         private int m_HeartbeatTimeout; | ||
|  |         private string m_PlayerWithTestsPath; | ||
|  |         private PlaymodeTestsController m_Runner; | ||
|  | 
 | ||
|  |         internal PlayerLauncherBuildOptions playerBuildOptions { get; private set; } | ||
|  | 
 | ||
|  |         public PlayerLauncher(PlaymodeTestsControllerSettings settings, BuildTarget? targetPlatform, ITestRunSettings overloadTestRunSettings, int heartbeatTimeout, string playerWithTestsPath, string scenePath, Scene scene, PlaymodeTestsController runner) : base(settings) | ||
|  |         { | ||
|  |             m_TargetPlatform = targetPlatform ?? EditorUserBuildSettings.activeBuildTarget; | ||
|  |             m_OverloadTestRunSettings = overloadTestRunSettings; | ||
|  |             m_HeartbeatTimeout = heartbeatTimeout; | ||
|  |             m_PlayerWithTestsPath = playerWithTestsPath; | ||
|  |             m_SceneName = scenePath; | ||
|  |             m_Scene = scene; | ||
|  |             m_Runner = runner; | ||
|  |         } | ||
|  | 
 | ||
|  |         protected override RuntimePlatform? TestTargetPlatform | ||
|  |         { | ||
|  |             get { return BuildTargetConverter.TryConvertToRuntimePlatform(m_TargetPlatform); } | ||
|  |         } | ||
|  | 
 | ||
|  |         public override void Run() | ||
|  |         { | ||
|  |             var editorConnectionTestCollector = RemoteTestRunController.instance; | ||
|  |             editorConnectionTestCollector.hideFlags = HideFlags.HideAndDontSave; | ||
|  |             editorConnectionTestCollector.Init(m_TargetPlatform, m_HeartbeatTimeout); | ||
|  | 
 | ||
|  |             var remotePlayerLogController = RemotePlayerLogController.instance; | ||
|  |             remotePlayerLogController.hideFlags = HideFlags.HideAndDontSave; | ||
|  | 
 | ||
|  |             using (var settings = new PlayerLauncherContextSettings(m_OverloadTestRunSettings)) | ||
|  |             { | ||
|  |                 PrepareScene(m_SceneName, m_Scene, m_Runner); | ||
|  | 
 | ||
|  |                 var filter = m_Settings.BuildNUnitFilter(); | ||
|  |                 var runner = LoadTests(filter); | ||
|  |                 var exceptionThrown = ExecutePreBuildSetupMethods(runner.LoadedTest, filter); | ||
|  |                 if (exceptionThrown) | ||
|  |                 { | ||
|  |                     ReopenOriginalScene(m_Settings.originalScene); | ||
|  |                     CallbacksDelegator.instance.RunFailed("Run Failed: One or more errors in a prebuild setup. See the editor log for details."); | ||
|  |                     return; | ||
|  |                 } | ||
|  | 
 | ||
|  |                 EditorSceneManager.MarkSceneDirty(m_Scene); | ||
|  |                 EditorSceneManager.SaveScene(m_Scene); | ||
|  | 
 | ||
|  |                 playerBuildOptions = GetBuildOptions(m_SceneName); | ||
|  |                 bool success = BuildAndRunFromLauncherOptions(playerBuildOptions); | ||
|  | 
 | ||
|  |                 FilePathMetaInfo.TryCreateFile(runner.LoadedTest, playerBuildOptions); | ||
|  |                 ExecutePostBuildCleanupMethods(runner.LoadedTest, filter); | ||
|  | 
 | ||
|  |                 ReopenOriginalScene(m_Settings.originalScene); | ||
|  | 
 | ||
|  |                 if (!success) | ||
|  |                 { | ||
|  |                     Object.DestroyImmediate(editorConnectionTestCollector); | ||
|  |                     Debug.LogError("Player build failed"); | ||
|  |                     throw new TestLaunchFailedException("Player build failed"); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 if ((playerBuildOptions.GetCurrentBuildOptions() & BuildOptions.AutoRunPlayer) != 0) | ||
|  |                 { | ||
|  |                     editorConnectionTestCollector.PostSuccessfulBuildAction(); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 var runSettings = m_OverloadTestRunSettings as PlayerLauncherTestRunSettings; | ||
|  |                 if (success && runSettings != null && runSettings.buildOnly) | ||
|  |                 { | ||
|  |                     EditorUtility.RevealInFinder(playerBuildOptions.BuildPlayerOptions.locationPathName); | ||
|  |                 } | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         public void PrepareScene(string sceneName, Scene scene, PlaymodeTestsController runner) | ||
|  |         { | ||
|  |             runner.AddEventHandlerMonoBehaviour<PlayModeRunnerCallback>(); | ||
|  |             var commandLineArgs = Environment.GetCommandLineArgs(); | ||
|  |             if (!commandLineArgs.Contains("-doNotReportTestResultsBackToEditor")) | ||
|  |             { | ||
|  |                 runner.AddEventHandlerMonoBehaviour<RemoteTestResultSender>(); | ||
|  |             } | ||
|  |             runner.AddEventHandlerMonoBehaviour<PlayerQuitHandler>(); | ||
|  |             runner.AddEventHandlerScriptableObject<TestRunCallbackListener>(); | ||
|  | 
 | ||
|  |             EditorSceneManager.MarkSceneDirty(scene); | ||
|  |             AssetDatabase.SaveAssets(); | ||
|  |             EditorSceneManager.SaveScene(scene, sceneName, false); | ||
|  |         } | ||
|  | 
 | ||
|  |         private bool BuildAndRunFromLauncherOptions(PlayerLauncherBuildOptions playerBuildOptions) | ||
|  |         { | ||
|  |             bool success = false; | ||
|  | 
 | ||
|  | #if UNITY_6000_1_OR_NEWER | ||
|  |             if (playerBuildOptions.ShouldBuildWithProfile()) | ||
|  |             { | ||
|  |                 playerBuildOptions.OnBeforeBuildProfileBuild(m_Scene.path); | ||
|  |                 Debug.LogFormat( | ||
|  |                     LogType.Log, LogOption.NoStacktrace, null, | ||
|  |                     "Building player with build profile options:\n{0}", playerBuildOptions); | ||
|  |                 success = BuildAndRunPlayer(playerBuildOptions.BuildPlayerWithProfileOptions); | ||
|  |                 playerBuildOptions.OnAfterBuildProfileBuild(); | ||
|  | 
 | ||
|  |             } | ||
|  |             else | ||
|  | #endif | ||
|  |             { | ||
|  |                 Debug.LogFormat( | ||
|  |                     LogType.Log, LogOption.NoStacktrace, null, | ||
|  |                     "Building player with following options:\n{0}", playerBuildOptions); | ||
|  |                 success = BuildAndRunPlayer(playerBuildOptions.BuildPlayerOptions); | ||
|  |             } | ||
|  | 
 | ||
|  |             return success; | ||
|  |         } | ||
|  | 
 | ||
|  |         private static bool BuildAndRunPlayer(BuildPlayerOptions buildOptions) | ||
|  |         { | ||
|  |             Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "Building player with following options:\n{0}", buildOptions); | ||
|  | 
 | ||
|  | #if !UNITY_2021_2_OR_NEWER | ||
|  |             // Android has to be in listen mode to establish player connection | ||
|  | 		    // Only flip connect to host if we are older than Unity 2021.2 | ||
|  |             if (buildOptions.BuildPlayerOptions.target == BuildTarget.Android) | ||
|  |             { | ||
|  |                 buildOptions.BuildPlayerOptions.options &= ~BuildOptions.ConnectToHost; | ||
|  |             } | ||
|  | #endif | ||
|  |             // For now, so does Lumin | ||
|  | #if !UNITY_2022_2_OR_NEWER | ||
|  |             if (buildOptions.BuildPlayerOptions.target == BuildTarget.Lumin) | ||
|  |             { | ||
|  |                 buildOptions.BuildPlayerOptions.options &= ~BuildOptions.ConnectToHost; | ||
|  |             } | ||
|  | #endif | ||
|  | 
 | ||
|  | #if UNITY_2023_2_OR_NEWER | ||
|  |             // WebGL has to be in close on quit mode to ensure that the browser tab is closed when the player finishes running tests | ||
|  |             if (buildOptions.target == BuildTarget.WebGL) | ||
|  |             { | ||
|  |                 PlayerSettings.WebGL.closeOnQuit = true; | ||
|  |             } | ||
|  | #endif | ||
|  | 
 | ||
|  |             var result = BuildPipeline.BuildPlayer(buildOptions); | ||
|  |             if (result.summary.result != BuildResult.Succeeded) | ||
|  |                 Debug.LogError(result.SummarizeErrors()); | ||
|  | 
 | ||
|  | #if UNITY_2023_2_OR_NEWER | ||
|  |             // Clean up WebGL close on quit mode | ||
|  |             if (buildOptions.target == BuildTarget.WebGL) | ||
|  |             { | ||
|  |                 PlayerSettings.WebGL.closeOnQuit = false; | ||
|  |             } | ||
|  | #endif | ||
|  | 
 | ||
|  |             return result.summary.result == BuildResult.Succeeded; | ||
|  |         } | ||
|  | 
 | ||
|  | #if UNITY_6000_1_OR_NEWER | ||
|  |         private static bool BuildAndRunPlayer(BuildPlayerWithProfileOptions buildOptions) | ||
|  |         { | ||
|  |             if (buildOptions.buildProfile == null) | ||
|  |             { | ||
|  |                 throw new TestLaunchFailedException("Player build failed, profile was null but BuildPlayerWithProfileOptions was used."); | ||
|  |             } | ||
|  | 
 | ||
|  |             Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "Building player with profile options:\n{0}", buildOptions); | ||
|  |             Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "Building with profile at path:\n{0}", | ||
|  |                 AssetDatabase.GetAssetPath(buildOptions.buildProfile)); | ||
|  | 
 | ||
|  |             // WebGL has to be in close on quit mode to ensure that the browser tab is closed when the player finishes running tests | ||
|  |             if (buildOptions.buildProfile.buildTarget == BuildTarget.WebGL) | ||
|  |             { | ||
|  |                 PlayerSettings.WebGL.closeOnQuit = true; | ||
|  |             } | ||
|  | 
 | ||
|  |             var result = BuildPipeline.BuildPlayer(buildOptions); | ||
|  |             if (result.summary.result != BuildResult.Succeeded) | ||
|  |                 Debug.LogError(result.SummarizeErrors()); | ||
|  | 
 | ||
|  |             // Clean up WebGL close on quit mode | ||
|  |             if (buildOptions.buildProfile.buildTarget == BuildTarget.WebGL) | ||
|  |             { | ||
|  |                 PlayerSettings.WebGL.closeOnQuit = false; | ||
|  |             } | ||
|  | 
 | ||
|  |             return result.summary.result == BuildResult.Succeeded; | ||
|  |         } | ||
|  | #endif | ||
|  | 
 | ||
|  | 
 | ||
|  |         internal PlayerLauncherBuildOptions GetBuildOptions(string scenePath) | ||
|  |         { | ||
|  |             var buildOnly = false; | ||
|  |             var runSettings = m_OverloadTestRunSettings as PlayerLauncherTestRunSettings; | ||
|  |             if (runSettings != null) | ||
|  |             { | ||
|  |                 buildOnly = runSettings.buildOnly; | ||
|  |             } | ||
|  | 
 | ||
|  |             var buildOptions = new BuildPlayerOptions(); | ||
|  | 
 | ||
|  |             var scenes = new List<string> { scenePath }; | ||
|  |             scenes.AddRange(EditorBuildSettings.scenes.Select(x => x.path)); | ||
|  |             buildOptions.scenes = scenes.ToArray(); | ||
|  | 
 | ||
|  |             buildOptions.options |= BuildOptions.Development | BuildOptions.ConnectToHost | BuildOptions.IncludeTestAssemblies | BuildOptions.StrictMode; | ||
|  |             buildOptions.target = m_TargetPlatform; | ||
|  | 
 | ||
|  | #if UNITY_2021_2_OR_NEWER | ||
|  |             buildOptions.subtarget = EditorUserBuildSettings.GetActiveSubtargetFor(m_TargetPlatform); | ||
|  | #endif | ||
|  | 
 | ||
|  |             if (EditorUserBuildSettings.waitForPlayerConnection) | ||
|  |                 buildOptions.options |= BuildOptions.WaitForPlayerConnection; | ||
|  | 
 | ||
|  |             if (EditorUserBuildSettings.allowDebugging) | ||
|  |                 buildOptions.options |= BuildOptions.AllowDebugging; | ||
|  | 
 | ||
|  |             if (EditorUserBuildSettings.installInBuildFolder) | ||
|  |                 buildOptions.options |= BuildOptions.InstallInBuildFolder; | ||
|  |             else if (!buildOnly) | ||
|  |                 buildOptions.options |= BuildOptions.AutoRunPlayer; | ||
|  | 
 | ||
|  |             var buildTargetGroup = EditorUserBuildSettings.activeBuildTargetGroup; | ||
|  |             buildOptions.targetGroup = buildTargetGroup; | ||
|  | 
 | ||
|  |             //Check if Lz4 is supported for the current buildtargetgroup and enable it if need be | ||
|  |             if (PostprocessBuildPlayer.SupportsLz4Compression(buildTargetGroup, m_TargetPlatform)) | ||
|  |             { | ||
|  |                 if (EditorUserBuildSettings.GetCompressionType(buildTargetGroup) == Compression.Lz4) | ||
|  |                     buildOptions.options |= BuildOptions.CompressWithLz4; | ||
|  |                 else if (EditorUserBuildSettings.GetCompressionType(buildTargetGroup) == Compression.Lz4HC) | ||
|  |                     buildOptions.options |= BuildOptions.CompressWithLz4HC; | ||
|  |             } | ||
|  | 
 | ||
|  |             string buildLocation; | ||
|  |             if (buildOnly) | ||
|  |             { | ||
|  |                 buildLocation = buildOptions.locationPathName = runSettings.buildOnlyLocationPath; | ||
|  |             } | ||
|  |             else | ||
|  |             { | ||
|  |                 var reduceBuildLocationPathLength = false; | ||
|  | 
 | ||
|  |                 //Some platforms hit MAX_PATH limits during the build process, in these cases minimize the path length | ||
|  |                 if ((m_TargetPlatform == BuildTarget.WSAPlayer) | ||
|  | #if !UNITY_2021_1_OR_NEWER | ||
|  |                 || (m_TargetPlatform == BuildTarget.XboxOne) | ||
|  | #endif | ||
|  | #if UNITY_2020_3_OR_NEWER | ||
|  |                 || (m_TargetPlatform == BuildTarget.GameCoreXboxSeries) || (m_TargetPlatform == BuildTarget.GameCoreXboxOne) | ||
|  | #endif | ||
|  |                 ) | ||
|  |                 { | ||
|  |                     reduceBuildLocationPathLength = true; | ||
|  |                 } | ||
|  | 
 | ||
|  |                 var uniqueTempPathInProject = FileUtil.GetUniqueTempPathInProject(); | ||
|  |                 var playerDirectoryName = "PlayerWithTests"; | ||
|  | 
 | ||
|  |                 //Some platforms hit MAX_PATH limits during the build process, in these cases minimize the path length | ||
|  |                 if (reduceBuildLocationPathLength) | ||
|  |                 { | ||
|  |                     playerDirectoryName = "PwT"; | ||
|  |                     uniqueTempPathInProject = Path.GetTempFileName(); | ||
|  |                     File.Delete(uniqueTempPathInProject); | ||
|  |                     Directory.CreateDirectory(uniqueTempPathInProject); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 buildLocation = Path.Combine(string.IsNullOrEmpty(m_PlayerWithTestsPath) ? Path.GetFullPath(uniqueTempPathInProject) : m_PlayerWithTestsPath, playerDirectoryName); | ||
|  | 
 | ||
|  |                 // iOS builds create a folder with Xcode project instead of an executable, therefore no executable name is added | ||
|  |                 if (m_TargetPlatform == BuildTarget.iOS) | ||
|  |                 { | ||
|  |                     buildOptions.locationPathName = buildLocation; | ||
|  |                 } | ||
|  |                 else | ||
|  |                 { | ||
|  |                     string extensionForBuildTarget = | ||
|  |                         PostprocessBuildPlayer.GetExtensionForBuildTarget(buildTargetGroup, buildOptions.target, | ||
|  |                             buildOptions.options); | ||
|  |                     var playerExecutableName = "PlayerWithTests"; | ||
|  | 
 | ||
|  |                     if (m_TargetPlatform == BuildTarget.GameCoreXboxSeries || | ||
|  |                         m_TargetPlatform == BuildTarget.GameCoreXboxOne) | ||
|  |                         PlayerSettings.productName = playerExecutableName; | ||
|  | 
 | ||
|  |                     if (!string.IsNullOrEmpty(extensionForBuildTarget)) | ||
|  |                         playerExecutableName += $".{extensionForBuildTarget}"; | ||
|  | 
 | ||
|  |                     buildOptions.locationPathName = Path.Combine(buildLocation, playerExecutableName); | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             var result = new PlayerLauncherBuildOptions | ||
|  |             { | ||
|  |                 BuildPlayerOptions = buildOptions, | ||
|  |                 PlayerDirectory = buildLocation, | ||
|  |             }; | ||
|  | 
 | ||
|  | #if UNITY_6000_1_OR_NEWER | ||
|  |             BuildPlayerWithProfileOptions options = new BuildPlayerWithProfileOptions(); | ||
|  |             options.locationPathName = buildOptions.locationPathName; | ||
|  |             options.assetBundleManifestPath = buildOptions.assetBundleManifestPath; | ||
|  |             options.options = buildOptions.options; | ||
|  |             options.buildProfile = null; | ||
|  |             result.BuildPlayerWithProfileOptions = options; | ||
|  | #endif | ||
|  | 
 | ||
|  |             return ModifyBuildOptions(result); | ||
|  |         } | ||
|  | 
 | ||
|  |         private PlayerLauncherBuildOptions ModifyBuildOptions(PlayerLauncherBuildOptions playerOptions) | ||
|  |         { | ||
|  |             var allAssemblies = AppDomain.CurrentDomain.GetAssemblies() | ||
|  |                 .Where(x => x.GetReferencedAssemblies().Any(z => z.Name == "UnityEditor.TestRunner")).ToArray(); | ||
|  |             var attributes = allAssemblies.SelectMany(assembly => assembly.GetCustomAttributes(typeof(TestPlayerBuildModifierAttribute), true).OfType<TestPlayerBuildModifierAttribute>()).ToArray(); | ||
|  |             var modifiers = attributes.Select(attribute => attribute.ConstructModifier()).ToArray(); | ||
|  | 
 | ||
|  |             foreach (var modifier in modifiers) | ||
|  |             { | ||
|  |                 playerOptions.BuildPlayerOptions = modifier.ModifyOptions(playerOptions.BuildPlayerOptions); | ||
|  | #if UNITY_6000_1_OR_NEWER | ||
|  |                 playerOptions.BuildPlayerWithProfileOptions = modifier.ModifyOptions(playerOptions.BuildPlayerWithProfileOptions); | ||
|  | #endif | ||
|  |             } | ||
|  | 
 | ||
|  |             return playerOptions; | ||
|  |         } | ||
|  | 
 | ||
|  |         private static bool ShouldReduceBuildLocationPathLength(BuildTarget target) | ||
|  |         { | ||
|  |             switch (target) | ||
|  |             { | ||
|  | #if UNITY_2020_2_OR_NEWER | ||
|  |                 case BuildTarget.GameCoreXboxOne: | ||
|  |                 case BuildTarget.GameCoreXboxSeries: | ||
|  | #else | ||
|  |                 case BuildTarget.XboxOne: | ||
|  | #endif | ||
|  |                 case BuildTarget.WSAPlayer: | ||
|  |                     return true; | ||
|  |                 default: | ||
|  |                     return false; | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | } |