using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using NUnit.Framework; using UnityEditor.Build.Content; using UnityEditor.Build.Pipeline.Injector; using UnityEditor.Build.Pipeline.Interfaces; using UnityEditor.Build.Pipeline.Tasks; using UnityEditor.Build.Pipeline.Utilities; using UnityEditor.Build.Pipeline.WriteTypes; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Build.Pipeline; using UnityEngine.SceneManagement; using UnityEngine.TestTools; namespace UnityEditor.Build.Pipeline.Tests { [TestFixture] class ScriptableBuildPipelineTests { const string k_FolderPath = "Test"; const string k_TmpPath = "tmp"; const string k_ScenePath = "Assets/testScene.unity"; const string k_TestAssetsPath = "Assets/TestAssetsOnlyWillBeDeleted"; const string k_CubePath = k_TestAssetsPath + "/Cube.prefab"; const string k_CubePath2 = k_TestAssetsPath + "/Cube2.prefab"; ScriptableBuildPipeline.Settings m_Settings; [OneTimeSetUp] public void Setup() { EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); Directory.CreateDirectory(k_TestAssetsPath); #if UNITY_2018_3_OR_NEWER PrefabUtility.SaveAsPrefabAsset(GameObject.CreatePrimitive(PrimitiveType.Cube), k_CubePath); PrefabUtility.SaveAsPrefabAsset(GameObject.CreatePrimitive(PrimitiveType.Cube), k_CubePath2); #else PrefabUtility.CreatePrefab(k_CubePath, GameObject.CreatePrimitive(PrimitiveType.Cube)); PrefabUtility.CreatePrefab(k_CubePath2, GameObject.CreatePrimitive(PrimitiveType.Cube)); #endif AssetDatabase.ImportAsset(k_CubePath); AssetDatabase.ImportAsset(k_CubePath2); m_Settings = LoadSettingsFromFile(); } [OneTimeTearDown] public void Cleanup() { EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); AssetDatabase.DeleteAsset(k_ScenePath); AssetDatabase.DeleteAsset(k_CubePath); AssetDatabase.DeleteAsset(k_CubePath2); AssetDatabase.DeleteAsset(k_TestAssetsPath); if (Directory.Exists(k_FolderPath)) Directory.Delete(k_FolderPath, true); if (Directory.Exists(k_TmpPath)) Directory.Delete(k_TmpPath, true); ScriptableBuildPipeline.s_Settings = m_Settings; ScriptableBuildPipeline.SaveSettings(); } static ReturnCode RunTask(params IContextObject[] args) where T : IBuildTask { IBuildContext context = new BuildContext(args); IBuildTask instance = Activator.CreateInstance(); ContextInjector.Inject(context, instance); var result = instance.Run(); ContextInjector.Extract(context, instance); return result; } static IBundleBuildParameters GetBuildParameters() { if (Directory.Exists(k_FolderPath)) Directory.Delete(k_FolderPath, true); if (Directory.Exists(k_TmpPath)) Directory.Delete(k_TmpPath, true); Directory.CreateDirectory(k_FolderPath); Directory.CreateDirectory(k_TmpPath); IBundleBuildParameters buildParams = new BundleBuildParameters(EditorUserBuildSettings.activeBuildTarget, EditorUserBuildSettings.selectedBuildTargetGroup, k_FolderPath); buildParams.TempOutputFolder = k_TmpPath; return buildParams; } static AssetBundleBuild CreateBundleBuild(string name, string path) { return new AssetBundleBuild() { addressableNames = new[] { path }, assetBundleName = name, assetBundleVariant = "", assetNames = new[] { path } }; } static IBundleBuildContent GetBundleContent(bool createAllBundles = false) { List buildData = new List(); buildData.Add(CreateBundleBuild("bundle", k_CubePath)); if (createAllBundles) buildData.Add(CreateBundleBuild("bundle2", k_CubePath2)); IBundleBuildContent buildContent = new BundleBuildContent(buildData); return buildContent; } static IDependencyData GetDependencyData() { GUID guid; GUID.TryParse(AssetDatabase.AssetPathToGUID(k_CubePath), out guid); ObjectIdentifier[] oId = ContentBuildInterface.GetPlayerObjectIdentifiersInAsset(guid, EditorUserBuildSettings.activeBuildTarget); AssetLoadInfo loadInfo = new AssetLoadInfo() { asset = guid, address = k_CubePath, includedObjects = oId.ToList(), referencedObjects = oId.ToList() }; IDependencyData dep = new BuildDependencyData(); dep.AssetInfo.Add(guid, loadInfo); dep.AssetUsage.Add(guid, new BuildUsageTagSet()); return dep; } [UnityTest] public IEnumerator BuildPipeline_AssetBundleBuild_DoesNotResetUnsavedScene() { Scene s = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); yield return null; EditorSceneManager.SaveScene(s, k_ScenePath); GameObject.CreatePrimitive(PrimitiveType.Cube); EditorSceneManager.MarkSceneDirty(s); GameObject objectWeAdded = GameObject.Find("Cube"); Assert.IsNotNull(objectWeAdded, "No object before entering playmode"); Assert.AreEqual("testScene", SceneManager.GetActiveScene().name); IBundleBuildParameters buildParameters = GetBuildParameters(); IBundleBuildContent buildContent = GetBundleContent(); IBundleBuildResults results; ReturnCode exitCode = ContentPipeline.BuildAssetBundles(buildParameters, buildContent, out results); Assert.AreEqual(ReturnCode.UnsavedChanges, exitCode); Assert.AreEqual("testScene", SceneManager.GetActiveScene().name); objectWeAdded = GameObject.Find("Cube"); Assert.IsNotNull(objectWeAdded, "No object after entering playmode"); EditorSceneManager.SaveScene(s, k_ScenePath); } [Test] public void BuildPipeline_AssetBundleBuild_WritesLinkXMLFile() { IBundleBuildParameters buildParameters = GetBuildParameters(); buildParameters.WriteLinkXML = true; IBundleBuildContent buildContent = GetBundleContent(); IBundleBuildResults results; ReturnCode exitCode = ContentPipeline.BuildAssetBundles(buildParameters, buildContent, out results); Assert.AreEqual(ReturnCode.Success, exitCode); var assemblies = new HashSet(); var types = new HashSet(); foreach (var writeResult in results.WriteResults) { foreach (var type in writeResult.Value.includedTypes) { assemblies.Add(type.Assembly); types.Add(type); } } var xml = LinkXMLGeneratorTests.ReadLinkXML(buildParameters.GetOutputFilePathForIdentifier("link.xml"), out int assemblyCount, out int typeCount); Assert.AreEqual(assemblyCount, assemblies.Count); Assert.AreEqual(typeCount, types.Count); foreach (var t in types) LinkXMLGeneratorTests.AssertTypePreserved(xml, t); } [Test] public void BuildPipeline_CompatibilityBuildPipeline_WritesManifest() { // Confirm that calling CompatibilityBuildPipeline.BuildAssetBundles generates a .manifest file similar to the top level file generated by BuildPipeline.BuildAssetBundles var buildParameters = GetBuildParameters() as BundleBuildParameters; string outputPath = buildParameters.OutputFolder; var bundleDefinitions = new AssetBundleBuild[2]; bundleDefinitions[0] = CreateBundleBuild("bundle", k_CubePath); bundleDefinitions[1] = CreateBundleBuild("bundle2", k_CubePath2); CompatibilityAssetBundleManifest manifestObject = CompatibilityBuildPipeline.BuildAssetBundles( outputPath, bundleDefinitions, BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.ForceRebuildAssetBundle, buildParameters.Target); Assert.IsNotNull(manifestObject); // .manifest file is created with name that matches the output build directory name var buildDirectoryName = Path.GetFileName(k_FolderPath); var expectedManifestPath = outputPath + "/" + buildDirectoryName + ".manifest"; // GetOutputFilePathForIdentifier is an alternative way to calculate the expected output path Assert.AreEqual(buildParameters.GetOutputFilePathForIdentifier(buildDirectoryName + ".manifest"), expectedManifestPath); FileAssert.Exists(expectedManifestPath); // Confirm the .manifest file contains the expected content string manifestContent = File.ReadAllText(expectedManifestPath); string expectedManifestContent = manifestObject.ToString(); Assert.AreEqual(expectedManifestContent, manifestContent); // Sanity check a few items of expected content Assert.IsTrue(manifestContent.Contains("bundle")); Assert.IsTrue(manifestContent.Contains("bundle2")); Assert.IsTrue(manifestContent.Contains("CRC:")); Assert.IsTrue(manifestContent.Contains("Hash:")); } [UnityTest] public IEnumerator ValidationMethods_HasDirtyScenes() { Scene s = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); yield return null; bool dirty = ValidationMethods.HasDirtyScenes(); Assert.IsFalse(dirty); EditorSceneManager.MarkSceneDirty(s); dirty = ValidationMethods.HasDirtyScenes(); Assert.IsTrue(dirty); } [Test] public void DefaultBuildTasks_WriteSerializedFiles() { IBuildParameters buildParams = GetBuildParameters(); IDependencyData dependencyData = new BuildDependencyData(); IWriteData writeData = new BuildWriteData(); IBuildResults results = new BuildResults(); ReturnCode exitCode = RunTask(buildParams, dependencyData, writeData, results); Assert.AreEqual(ReturnCode.Success, exitCode); } [Test] public void DefaultBuildTasks_GenerateBundlePacking() { IBundleBuildContent buildContent = GetBundleContent(); IDependencyData dep = GetDependencyData(); IBundleWriteData writeData = new BundleWriteData(); IDeterministicIdentifiers deterministicId = new PrefabPackedIdentifiers(); ReturnCode exitCode = RunTask(buildContent, dep, writeData, deterministicId); Assert.AreEqual(ReturnCode.Success, exitCode); } [Test] public void DefaultBuildTasks_GenerateBundleCommands() { IBundleBuildContent buildContent = GetBundleContent(); IDependencyData dep = GetDependencyData(); IBundleWriteData writeData = new BundleWriteData(); IDeterministicIdentifiers deterministicId = new PrefabPackedIdentifiers(); RunTask(buildContent, dep, writeData, deterministicId); IBundleBuildParameters buildParams = GetBuildParameters(); ReturnCode exitCode = RunTask(buildParams, buildContent, dep, writeData, deterministicId); Assert.AreEqual(ReturnCode.Success, exitCode); } [Test] public void DefaultBuildTasks_GenerateBundleMaps() { IDependencyData dep = GetDependencyData(); IBundleWriteData writeData = new BundleWriteData(); ReturnCode exitCode = RunTask(dep, writeData); Assert.AreEqual(ReturnCode.Success, exitCode); } [Test] public void DefaultBuildTasks_PostPackingCallback() { bool packingCallbackCalled = false; IBuildParameters buildParams = GetBuildParameters(); IDependencyData dep = GetDependencyData(); IBundleWriteData writeData = new BundleWriteData(); BuildCallbacks callback = new BuildCallbacks(); callback.PostPackingCallback = (parameters, data, arg3) => { packingCallbackCalled = true; return ReturnCode.Success; }; ReturnCode exitCode = RunTask(buildParams, dep, writeData, callback); Assert.AreEqual(ReturnCode.Success, exitCode); Assert.IsTrue(packingCallbackCalled); } [Test] public void DefaultBuildTasks_PostWritingCallback() { bool writingCallbackCalled = false; IBuildParameters buildParams = GetBuildParameters(); IDependencyData dep = GetDependencyData(); IWriteData writeData = new BuildWriteData(); IBuildResults results = new BuildResults(); BuildCallbacks callback = new BuildCallbacks(); callback.PostWritingCallback = (parameters, data, arg3, arg4) => { writingCallbackCalled = true; return ReturnCode.Success; }; ReturnCode exitCode = RunTask(buildParams, dep, writeData, results, callback); Assert.AreEqual(ReturnCode.Success, exitCode); Assert.IsTrue(writingCallbackCalled); } [Test] public void DefaultBuildTasks_PostDependencyCallback() { bool dependencyCallbackCalled = false; IBuildParameters buildParameters = GetBuildParameters(); IDependencyData dep = GetDependencyData(); BuildCallbacks callback = new BuildCallbacks(); callback.PostDependencyCallback = (parameters, data) => { dependencyCallbackCalled = true; return ReturnCode.Success; }; ReturnCode exitCode = RunTask(buildParameters, dep, callback); Assert.AreEqual(ReturnCode.Success, exitCode); Assert.IsTrue(dependencyCallbackCalled); } [Test] public void DefaultBuildTasks_PostScriptsCallbacks() { bool scriptsCallbackCalled = false; IBuildParameters buildParameters = GetBuildParameters(); IBuildResults results = new BuildResults(); BuildCallbacks callback = new BuildCallbacks(); callback.PostScriptsCallbacks = (parameters, buildResults) => { scriptsCallbackCalled = true; return ReturnCode.Success; }; ReturnCode exitCode = RunTask(buildParameters, results, callback); Assert.AreEqual(ReturnCode.Success, exitCode); Assert.IsTrue(scriptsCallbackCalled); } [Test] public void DefaultBuildTasks_AppendBundleHash() { IBundleBuildParameters buildParameters = GetBuildParameters(); buildParameters.AppendHash = true; var fileName = k_FolderPath + "/TestBundle"; var fileHash = HashingMethods.Calculate(fileName).ToHash128(); File.WriteAllText(fileName, fileName); IBundleBuildResults results = new BundleBuildResults(); results.BundleInfos["TestBundle"] = new BundleDetails { Crc = 0, FileName = fileName, Hash = fileHash }; ReturnCode exitCode = RunTask(buildParameters, results); Assert.AreEqual(ReturnCode.Success, exitCode); FileAssert.Exists(fileName + "_" + fileHash); } [UnityTest] public IEnumerator SceneDataWriteOperation_HashChanges_WhenPrefabDepenencyChanges() { Scene s = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); yield return null; var prefab = AssetDatabase.LoadAssetAtPath(k_CubePath); prefab.transform.position = new Vector3(0, 0, 0); EditorUtility.SetDirty(prefab); AssetDatabase.SaveAssets(); PrefabUtility.InstantiatePrefab(prefab); EditorSceneManager.SaveScene(s, k_ScenePath); var op = new SceneDataWriteOperation { Command = new WriteCommand(), PreloadInfo = new PreloadInfo(), #if !UNITY_2019_3_OR_NEWER ProcessedScene = k_ScenePath, #endif ReferenceMap = new BuildReferenceMap(), UsageSet = new BuildUsageTagSet(), Scene = k_ScenePath, DependencyHash = AssetDatabase.GetAssetDependencyHash(k_CubePath) }; var cacheVersion1 = op.GetHash128(); prefab.transform.position = new Vector3(1, 1, 1); EditorUtility.SetDirty(prefab); AssetDatabase.SaveAssets(); op.DependencyHash = AssetDatabase.GetAssetDependencyHash(k_CubePath); var cacheVersion2 = op.GetHash128(); Assert.AreNotEqual(cacheVersion1, cacheVersion2); } [UnityTest] public IEnumerator SceneBundleWriteOperation_HashChanges_WhenPrefabDepenencyChanges() { Scene s = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); yield return null; var prefab = AssetDatabase.LoadAssetAtPath(k_CubePath); prefab.transform.position = new Vector3(0, 0, 0); EditorUtility.SetDirty(prefab); AssetDatabase.SaveAssets(); PrefabUtility.InstantiatePrefab(prefab); EditorSceneManager.SaveScene(s, k_ScenePath); var op = new SceneBundleWriteOperation { Command = new WriteCommand(), PreloadInfo = new PreloadInfo(), #if !UNITY_2019_3_OR_NEWER ProcessedScene = k_ScenePath, #endif ReferenceMap = new BuildReferenceMap(), UsageSet = new BuildUsageTagSet(), Info = new SceneBundleInfo(), Scene = k_ScenePath, DependencyHash = AssetDatabase.GetAssetDependencyHash(k_CubePath) }; var cacheVersion1 = op.GetHash128(); prefab.transform.position = new Vector3(1, 1, 1); EditorUtility.SetDirty(prefab); AssetDatabase.SaveAssets(); op.DependencyHash = AssetDatabase.GetAssetDependencyHash(k_CubePath); var cacheVersion2 = op.GetHash128(); Assert.AreNotEqual(cacheVersion1, cacheVersion2); } [Test] public void BuildAssetBundles_WhenNoBuildInContextLog_CreatesPerformanceLogReport() { IBundleBuildParameters buildParameters = GetBuildParameters(); IBundleBuildContent buildContent = GetBundleContent(); IBundleBuildResults results; buildParameters.Group = EditorUserBuildSettings.selectedBuildTargetGroup; ContentPipeline.BuildAssetBundles(buildParameters, buildContent, out results); string tepBuildLog = buildParameters.GetOutputFilePathForIdentifier("buildlogtep.json"); FileAssert.Exists(tepBuildLog); } [Test] public void BuildAssetBundles_WhenBuildLogProvided_DoesNotCreatePerformanceLogReport() { IBundleBuildParameters buildParameters = GetBuildParameters(); IBundleBuildContent buildContent = GetBundleContent(); IBundleBuildResults results; var taskList = DefaultBuildTasks.Create(DefaultBuildTasks.Preset.AssetBundleCompatible); ContentPipeline.BuildAssetBundles(buildParameters, buildContent, out results, taskList, new BuildLog()); string tepBuildLog = buildParameters.GetOutputFilePathForIdentifier("buildlogtep.json"); FileAssert.DoesNotExist(tepBuildLog); } [Test] public void BuildParameters_SetsBuildCacheServerParameters_WhenUseBuildCacheServerEnabled() { int port = 9999; string host = "fake host"; using (new StoreCacheServerConfig(true, host, port)) { IBundleBuildParameters buildParameters = GetBuildParameters(); Assert.AreEqual(port, buildParameters.CacheServerPort); Assert.AreEqual(host, buildParameters.CacheServerHost); } } [Test] public void BuildParameters_DoesNotSetBuildCacheServerParameters_WhenUseBuildCacheServerDisabled() { int port = 9999; string host = "testHost"; using (new StoreCacheServerConfig(false, host, port)) { IBundleBuildParameters buildParameters = GetBuildParameters(); Assert.AreEqual(8126, buildParameters.CacheServerPort); Assert.AreEqual(null, buildParameters.CacheServerHost); } } [Test] public void BuildAssetBundles_WithCache_Succeeds() { IBundleBuildParameters buildParameters = GetBuildParameters(); IBundleBuildContent buildContent = GetBundleContent(true); ReturnCode exitCode = ContentPipeline.BuildAssetBundles(buildParameters, buildContent, out IBundleBuildResults results); Assert.AreEqual(ReturnCode.Success, exitCode); } [Test] public void BuildAssetBundles_WithoutCache_Succeeds() { IBundleBuildParameters buildParameters = GetBuildParameters(); buildParameters.UseCache = false; IBundleBuildContent buildContent = GetBundleContent(true); ReturnCode exitCode = ContentPipeline.BuildAssetBundles(buildParameters, buildContent, out IBundleBuildResults results); Assert.AreEqual(ReturnCode.Success, exitCode); } ScriptableBuildPipeline.Settings LoadSettingsFromFile() { var settings = new ScriptableBuildPipeline.Settings(); if (File.Exists(ScriptableBuildPipeline.kSettingPath)) { var json = File.ReadAllText(ScriptableBuildPipeline.kSettingPath); EditorJsonUtility.FromJsonOverwrite(json, settings); } return settings; } void AssertSettingsChanged(ScriptableBuildPipeline.Settings preSettings, ScriptableBuildPipeline.Settings postSettings, FieldInfo[] fields, int fieldChanged) { for (int i = 0; i < fields.Length; i++) { if (i == fieldChanged) Assert.AreNotEqual(fields[i].GetValue(preSettings), fields[i].GetValue(postSettings), $"Unexpected field '{fields[i].Name}' value is unchanged."); else Assert.AreEqual(fields[i].GetValue(preSettings), fields[i].GetValue(postSettings), $"Unexpected field '{fields[i].Name}' value is changed."); } } [Test] public void PreferencesProperties_ChangesSerializeToDisk() { PropertyInfo[] properties = typeof(ScriptableBuildPipeline).GetProperties(BindingFlags.Public | BindingFlags.Static); FieldInfo[] fields = typeof(ScriptableBuildPipeline.Settings).GetFields(BindingFlags.Public | BindingFlags.Instance); for (int i = 0; i < properties.Length; i++) { var preSettings = LoadSettingsFromFile(); if (properties[i].PropertyType == typeof(bool)) { bool previousValue = (bool)fields[i].GetValue(preSettings); properties[i].SetValue(null, !previousValue); } else if (properties[i].PropertyType == typeof(int)) { int previousValue = (int)fields[i].GetValue(preSettings); properties[i].SetValue(null, previousValue + 5); } else if (properties[i].PropertyType == typeof(string)) { string previousValue = (string)fields[i].GetValue(preSettings); properties[i].SetValue(null, previousValue + "Test"); } else { Assert.Fail($"Unhandled property type '{properties[i].PropertyType.Name}' in test '{nameof(PreferencesProperties_ChangesSerializeToDisk)}'"); } AssertSettingsChanged(preSettings, LoadSettingsFromFile(), fields, i); } } internal class StoreCacheServerConfig : IDisposable { private bool m_StoredUseServerFlag; private string m_StoredServerHost; private int m_UseServerPort; public StoreCacheServerConfig(bool useCacheServer, string host, int port) { m_StoredUseServerFlag = ScriptableBuildPipeline.UseBuildCacheServer; ScriptableBuildPipeline.UseBuildCacheServer = useCacheServer; m_StoredServerHost = ScriptableBuildPipeline.CacheServerHost; ScriptableBuildPipeline.CacheServerHost = host; m_UseServerPort = ScriptableBuildPipeline.CacheServerPort; ScriptableBuildPipeline.CacheServerPort = port; } public void Dispose() { ScriptableBuildPipeline.UseBuildCacheServer = m_StoredUseServerFlag; ScriptableBuildPipeline.CacheServerHost = m_StoredServerHost; ScriptableBuildPipeline.CacheServerPort = m_UseServerPort; } } } }