using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using NUnit.Framework.Internal.Filters; using UnityEditor; 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; 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 int m_HeartbeatTimeout; private string m_PlayerWithTestsPath; public PlayerLauncher(PlaymodeTestsControllerSettings settings, BuildTarget? targetPlatform, ITestRunSettings overloadTestRunSettings, int heartbeatTimeout, string playerWithTestsPath) : base(settings) { m_TargetPlatform = targetPlatform ?? EditorUserBuildSettings.activeBuildTarget; m_OverloadTestRunSettings = overloadTestRunSettings; m_HeartbeatTimeout = heartbeatTimeout; m_PlayerWithTestsPath = playerWithTestsPath; } 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)) { m_SceneName = CreateSceneName(); var scene = PrepareScene(m_SceneName); string scenePath = scene.path; var filter = m_Settings.BuildNUnitFilter(); var runner = LoadTests(filter); var exceptionThrown = ExecutePreBuildSetupMethods(runner.LoadedTest, filter); if (exceptionThrown) { ReopenOriginalScene(m_Settings.originalScene); AssetDatabase.DeleteAsset(m_SceneName); CallbacksDelegator.instance.RunFailed("Run Failed: One or more errors in a prebuild setup. See the editor log for details."); return; } EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); var playerBuildOptions = GetBuildOptions(scenePath); var success = BuildAndRunPlayer(playerBuildOptions); editorConnectionTestCollector.PostBuildAction(); ExecutePostBuildCleanupMethods(runner.LoadedTest, filter); ReopenOriginalScene(m_Settings.originalScene); AssetDatabase.DeleteAsset(m_SceneName); if (!success) { editorConnectionTestCollector.CleanUp(); ScriptableObject.DestroyImmediate(editorConnectionTestCollector); Debug.LogError("Player build failed"); throw new TestLaunchFailedException("Player build failed"); } if ((playerBuildOptions.BuildPlayerOptions.options & BuildOptions.AutoRunPlayer) != 0) { editorConnectionTestCollector.PostSuccessfulBuildAction(); editorConnectionTestCollector.PostSuccessfulLaunchAction(); } var runSettings = m_OverloadTestRunSettings as PlayerLauncherTestRunSettings; if (success && runSettings != null && runSettings.buildOnly) { EditorUtility.RevealInFinder(playerBuildOptions.BuildPlayerOptions.locationPathName); } } } public Scene PrepareScene(string sceneName) { var scene = CreateBootstrapScene(sceneName, runner => { runner.AddEventHandlerMonoBehaviour(); runner.settings = m_Settings; var commandLineArgs = Environment.GetCommandLineArgs(); if (!commandLineArgs.Contains("-doNotReportTestResultsBackToEditor")) { runner.AddEventHandlerMonoBehaviour(); } runner.AddEventHandlerMonoBehaviour(); runner.AddEventHandlerScriptableObject(); }); return scene; } private static bool BuildAndRunPlayer(PlayerLauncherBuildOptions 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 (buildOptions.BuildPlayerOptions.target == BuildTarget.Lumin) { buildOptions.BuildPlayerOptions.options &= ~BuildOptions.ConnectToHost; } var result = BuildPipeline.BuildPlayer(buildOptions.BuildPlayerOptions); if (result.summary.result != Build.Reporting.BuildResult.Succeeded) Debug.LogError(result.SummarizeErrors()); return result.summary.result == Build.Reporting.BuildResult.Succeeded; } 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() { 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 ) { 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 (!string.IsNullOrEmpty(extensionForBuildTarget)) playerExecutableName += $".{extensionForBuildTarget}"; buildOptions.locationPathName = Path.Combine(buildLocation, playerExecutableName); } } return new PlayerLauncherBuildOptions { BuildPlayerOptions = ModifyBuildOptions(buildOptions), PlayerDirectory = buildLocation, }; } private BuildPlayerOptions ModifyBuildOptions(BuildPlayerOptions buildOptions) { 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()).ToArray(); var modifiers = attributes.Select(attribute => attribute.ConstructModifier()).ToArray(); foreach (var modifier in modifiers) { buildOptions = modifier.ModifyOptions(buildOptions); } return buildOptions; } 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; } } } }