using NUnit.Framework; using System; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEditor.AddressableAssets.Build.BuildPipelineTasks; using UnityEditor.AddressableAssets.Build.DataBuilders; using UnityEditor.AddressableAssets.Build.Layout; using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEditor.AddressableAssets.Tests; using UnityEditor.Build.Pipeline.Utilities; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.U2D; using UnityEditor.U2D; using UnityEditor.Presets; public class BuildLayoutGenerationTaskTests { AddressableAssetSettings m_Settings; AddressableAssetSettings Settings { get { if (m_Settings == null) { var path = Path.Combine(TempPath, "Settings", "/AddressableAssetSettings.Tests.asset"); m_Settings = AssetDatabase.LoadAssetAtPath(path); } return m_Settings; } } static string kTempPath = "Assets/BuildLayoutGenerationTaskTestsData"; static string TempPath; static int ExecCount; bool m_PrevGenerateBuildLayout; ProjectConfigData.ReportFileFormat m_PrevFileFormat; [OneTimeSetUp] public void OneTimeSetup() { ExecCount = 0; } [SetUp] public void Setup() { TempPath = kTempPath + (ExecCount++).ToString(); foreach (var fileFormat in Enum.GetValues(typeof(ProjectConfigData.ReportFileFormat))) { string layoutFile = BuildLayoutGenerationTask.GetLayoutFilePathForFormat((ProjectConfigData.ReportFileFormat)fileFormat); if (File.Exists(layoutFile)) File.Delete(layoutFile); } m_PrevGenerateBuildLayout = ProjectConfigData.GenerateBuildLayout; m_PrevFileFormat = ProjectConfigData.BuildLayoutReportFileFormat; BuildScriptPackedMode.s_SkipCompilePlayerScripts = true; ProjectConfigData.GenerateBuildLayout = true; if (Directory.Exists(TempPath)) Directory.Delete(TempPath, true); Directory.CreateDirectory(TempPath); m_Settings = AddressableAssetSettings.Create(Path.Combine(TempPath, "Settings"), "AddressableAssetSettings.Tests", false, true); } [TearDown] public void Teardown() { BuildScriptPackedMode.s_SkipCompilePlayerScripts = false; ProjectConfigData.GenerateBuildLayout = m_PrevGenerateBuildLayout; ProjectConfigData.BuildLayoutReportFileFormat = m_PrevFileFormat; // Many of the tests keep recreating assets in the same path, so we need to unload them completely so they don't get reused by the next test AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(Settings)); Resources.UnloadAsset(Settings); FileUtil.DeleteFileOrDirectory(TempPath); FileUtil.DeleteFileOrDirectory(TempPath + ".meta"); AssetDatabase.Refresh(); } string MakeAddressable(AddressableAssetGroup group, string guid, string address = null) { var entry = Settings.CreateOrMoveEntry(guid, group, false, false); entry.address = address == null ? Path.GetFileNameWithoutExtension(entry.AssetPath) : address; entry.BundleFileId = "GenericFileId"; return guid; } // Prefab asset emthods static string CreatePrefabAsset(string name) { return CreatePrefabAsset($"{TempPath}/{name}.prefab", name); } static string CreatePrefabAsset(string assetPath, string objectName) { GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.name = objectName; //this is to ensure that bundles are different for every run. go.transform.localPosition = UnityEngine.Random.onUnitSphere; PrefabUtility.SaveAsPrefabAsset(go, assetPath); UnityEngine.Object.DestroyImmediate(go, false); return AssetDatabase.AssetPathToGUID(assetPath); } static string CreateScriptableObjectAsset(string assetPath, string objectName) { TestObject.Create(objectName, assetPath); return AssetDatabase.AssetPathToGUID(assetPath); } string CreateAddressablePrefab(string name, AddressableAssetGroup group) { string guid = CreatePrefabAsset($"{TempPath}/{name}.prefab", name); return MakeAddressable(group, guid); } string CreateAddressableScriptableObject(string name, AddressableAssetGroup group) { string guid = CreateScriptableObjectAsset($"{TempPath}/{name}.asset", name); return MakeAddressable(group, guid); } bool DeletePrefab(string name) { string path = $"{TempPath}/{name}.prefab"; return AssetDatabase.DeleteAsset(path); } bool DeleteScriptableObject(string name) { string path = $"{TempPath}/{name}.asset"; return AssetDatabase.DeleteAsset(path); } // Texture asset creation static string CreateTexture(string name, int size = 32) { string assetPath = $"{TempPath}/{name}.png"; var texture = new Texture2D(size, size); var data = ImageConversion.EncodeToPNG(texture); UnityEngine.Object.DestroyImmediate(texture); File.WriteAllBytes(assetPath, data); AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); return AssetDatabase.AssetPathToGUID(assetPath); } string CreateAddressableTexture(string name, AddressableAssetGroup group, int size = 32) { string guid = CreateTexture(name, size); TextureImporter ti = (TextureImporter)AssetImporter.GetAtPath(AssetDatabase.GUIDToAssetPath(guid)); ti.isReadable = false; ti.SaveAndReimport(); return MakeAddressable(group, guid); } static string CreateSpriteAtlas(string name, string guidTargetTexture) { var sa = new SpriteAtlas(); var targetObjects = new UnityEngine.Object[] {AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guidTargetTexture))}; sa.Add(targetObjects); string saPath = $"{TempPath}/{name}.spriteAtlas"; AssetDatabase.CreateAsset(sa, saPath); AssetDatabase.Refresh(); return AssetDatabase.AssetPathToGUID(saPath); } bool DeleteSpriteAtlas(string name) { string assetPath = $"{TempPath}/{name}.spriteAtlas"; return AssetDatabase.DeleteAsset(assetPath); } static string CreateSpriteTexture(string name, int size, bool includesSource) { string guid = CreateTexture(name, size); string texturePath = AssetDatabase.GUIDToAssetPath(guid); TextureImporter importer = (TextureImporter)AssetImporter.GetAtPath(AssetDatabase.GUIDToAssetPath(guid)); importer.textureType = TextureImporterType.Sprite; // creates a sprite subobject importer.spriteImportMode = SpriteImportMode.Single; importer.SaveAndReimport(); return guid; } bool DeleteTexture(string name) { string assetPath = $"{TempPath}/{name}.png"; return AssetDatabase.DeleteAsset(assetPath); } /// /// Adds a component to Prefab that references assetToReference /// /// /// void MakePefabReference(string prefabGUID, string assetToReferenceGUID) { GameObject prefab = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(prefabGUID)); UnityEngine.Object target = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(assetToReferenceGUID)); prefab.AddComponent().Reference = target; } AddressableAssetGroup CreateGroup(string name) { return Settings.CreateGroup(name, false, false, false, null, typeof(BundledAssetGroupSchema)); } void PrintText(BuildLayout layout) { MemoryStream stream = new MemoryStream(); BuildLayoutPrinter.WriteBundleLayout(stream, layout); string report = Encoding.ASCII.GetString(stream.ToArray()); Debug.Log(report); } internal BuildLayout BuildAndExtractLayout() { try { BuildLayout layout = null; BuildLayoutGenerationTask.s_LayoutCompleteCallback = (x, y) => layout = y; Settings.BuildPlayerContentImpl(); return layout; } finally { BuildLayoutGenerationTask.s_LayoutCompleteCallback = null; } } class WebExtractSession : IDisposable { public string DataDirectory; public string[] Files; public WebExtractSession(string filePath) { DataDirectory = filePath + "_data"; if (Directory.Exists(DataDirectory)) throw new Exception("Bundle data directory already exists"); var baseDir = Path.GetDirectoryName(EditorApplication.applicationPath); var webExtractFiles = Directory.GetFiles(baseDir, "WebExtract*", SearchOption.AllDirectories); string webExtractPath = webExtractFiles[0]; Assert.IsTrue(File.Exists(filePath), "Param filePath does not point to an existing file."); var process = new System.Diagnostics.Process { StartInfo = { FileName = webExtractPath, Arguments = string.Format(@"""{0}""", filePath), UseShellExecute = false, RedirectStandardOutput = true } }; process.Start(); var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); var exitCode = process.ExitCode; process.Close(); Assert.AreEqual(0, exitCode); Files = Directory.GetFiles(DataDirectory); } public void Dispose() { Directory.Delete(DataDirectory, true); } } internal void AssertEditorBundleDetailsMatchPhysicalBundle(string bundlePath, BuildLayout.Bundle bundle) { Assert.AreEqual(new FileInfo(bundlePath).Length, bundle.FileSize); using (var wes = new WebExtractSession(bundlePath)) { Assert.AreEqual(bundle.Files.Sum(x => x.SubFiles.Count), wes.Files.Length); foreach (BuildLayout.SubFile sf in bundle.Files.SelectMany(x => x.SubFiles)) { string filename = Path.Combine(wes.DataDirectory, sf.Name); Assert.AreEqual(sf.Size, new FileInfo(filename).Length); } } } [Test] public void WhenBundleReferencesAnotherBundle_ExternalReferenceExists() { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(ProjectConfigData.BuildLayoutReportFileFormat); AddressableAssetGroup group = null; AddressableAssetGroup group2 = null; try { // setup group = CreateGroup("Group1"); string prefabGUID = CreateAddressablePrefab("p1", group); group2 = CreateGroup("Group2"); string g2p1GUID = CreateAddressablePrefab("g2p1", group2); MakePefabReference(prefabGUID, g2p1GUID); AssetDatabase.SaveAssets(); BuildLayout layout = BuildAndExtractLayout(); // Test CollectionAssert.Contains(layout.Groups[0].Bundles[0].Dependencies, layout.Groups[1].Bundles[0]); Assert.AreEqual(layout.Groups[0].Bundles[0].Files[0].Assets[0].ExternallyReferencedAssets[0], layout.Groups[1].Bundles[0].Files[0].Assets[0]); } finally // cleanup { if (group != null) Settings.RemoveGroup(group); if (group2 != null) Settings.RemoveGroup(group2); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeletePrefab("p1"); DeletePrefab("g2p1"); } } [Test] public void WhenAssetImplicitlyPulledIntoBundle_ImplicitEntryAndReferencesCreated() { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(ProjectConfigData.BuildLayoutReportFileFormat); AddressableAssetGroup group = null; try { // setup group = CreateGroup("Group1"); string prefabGUID = CreateAddressablePrefab("p1", group); string aGUID = CreatePrefabAsset("p2"); MakePefabReference(prefabGUID, aGUID); AssetDatabase.SaveAssets(); BuildLayout layout = BuildAndExtractLayout(); // Test BuildLayout.DataFromOtherAsset oa = layout.Groups[0].Bundles[0].Files[0].OtherAssets.First(x => x.AssetPath.Contains("p2.prefab")); Assert.AreEqual(aGUID, oa.AssetGuid); } finally // cleanup { if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeletePrefab("p1"); DeletePrefab("p2"); } } [Test] public void WhenBundleContainsMultipleFiles_FilesAndSizesMatchArchiveContent() { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(ProjectConfigData.BuildLayoutReportFileFormat); string scenePath = $"{TempPath}/scene.unity"; AddressableAssetGroup groupScenes = null; AddressableAssetGroup textureGroup = null; try { // setup groupScenes = CreateGroup("SceneGroup"); textureGroup = CreateGroup("TextureGroup"); Scene scene1 = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); new GameObject().AddComponent(); EditorSceneManager.SaveScene(scene1, scenePath); Settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(scenePath), groupScenes); EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); CreateAddressableTexture("t1", textureGroup, 256); AssetDatabase.SaveAssets(); BuildLayout layout = BuildAndExtractLayout(); // Test BundledAssetGroupSchema schema = Settings.groups.First(x => x.HasSchema()).GetSchema(); string path = schema.BuildPath.GetValue(Settings); foreach (BuildLayout.Bundle bundle in layout.Groups.SelectMany(x => x.Bundles)) AssertEditorBundleDetailsMatchPhysicalBundle(Path.Combine(path, bundle.Name), bundle); } finally // cleanup { if (groupScenes != null) Settings.RemoveGroup(groupScenes); if (textureGroup != null) Settings.RemoveGroup(textureGroup); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); AssetDatabase.DeleteAsset(scenePath); DeleteTexture("t1"); } } // Even though slim writes is true, the system will enable it if it needs to generate a build layout report [Test] public void WhenSlimWriteResultsIsTrue_LayoutStillGenerated() { ProjectConfigData.ReportFileFormat fileFormat = ProjectConfigData.ReportFileFormat.TXT; string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(fileFormat); AddressableAssetGroup group = null; bool prevSlim = ScriptableBuildPipeline.slimWriteResults; ProjectConfigData.ReportFileFormat prevFileFormat = ProjectConfigData.BuildLayoutReportFileFormat; try { // setup ScriptableBuildPipeline.slimWriteResults = true; ProjectConfigData.BuildLayoutReportFileFormat = fileFormat; group = CreateGroup("Group1"); CreateAddressablePrefab("p1", group); AssetDatabase.SaveAssets(); BuildAndExtractLayout(); FileAssert.Exists(layoutFilePath); } finally // cleanup { ScriptableBuildPipeline.slimWriteResults = prevSlim; ProjectConfigData.BuildLayoutReportFileFormat = prevFileFormat; if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeletePrefab("p1"); } } [Test] public void WhenBuildLayoutIsDisabled_BuildLayoutIsNotGenerated() { ProjectConfigData.ReportFileFormat fileFormat = ProjectConfigData.ReportFileFormat.TXT; string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(fileFormat); AddressableAssetGroup group = null; bool prevGenerateBuildLayout = ProjectConfigData.GenerateBuildLayout; ProjectConfigData.ReportFileFormat prevFileFormat = ProjectConfigData.BuildLayoutReportFileFormat; try { // setup ProjectConfigData.GenerateBuildLayout = false; ProjectConfigData.BuildLayoutReportFileFormat = fileFormat; group = CreateGroup("Group1"); CreateAddressablePrefab("p1", group); AssetDatabase.SaveAssets(); BuildAndExtractLayout(); // Test FileAssert.DoesNotExist(layoutFilePath); } finally // cleanup { ProjectConfigData.GenerateBuildLayout = prevGenerateBuildLayout; ProjectConfigData.BuildLayoutReportFileFormat = prevFileFormat; if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeletePrefab("p1"); } } [Test] [TestCase(ProjectConfigData.ReportFileFormat.TXT)] [TestCase(ProjectConfigData.ReportFileFormat.JSON)] public void WhenBuildLayoutIsEnabled_BuildLayoutIsGenerated(ProjectConfigData.ReportFileFormat format) { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(format); AddressableAssetGroup group = null; bool prevGenerateBuildLayout = ProjectConfigData.GenerateBuildLayout; ProjectConfigData.ReportFileFormat prevFileFormat = ProjectConfigData.BuildLayoutReportFileFormat; try { // setup ProjectConfigData.GenerateBuildLayout = true; ProjectConfigData.BuildLayoutReportFileFormat = format; group = CreateGroup("Group1"); CreateAddressablePrefab("p1", group); AssetDatabase.SaveAssets(); BuildAndExtractLayout(); // Test FileAssert.Exists(layoutFilePath); if (format == ProjectConfigData.ReportFileFormat.JSON) { string text = File.ReadAllText(layoutFilePath); var layout = JsonUtility.FromJson(text); Assert.IsNotNull(layout); } } finally // cleanup { ProjectConfigData.GenerateBuildLayout = prevGenerateBuildLayout; ProjectConfigData.BuildLayoutReportFileFormat = prevFileFormat; if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeletePrefab("p1"); } } [Test] public void WhenAssetHasStreamedData_IsReportedCorrectly() { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(ProjectConfigData.BuildLayoutReportFileFormat); AddressableAssetGroup group = null; try { // setup group = CreateGroup("Group1"); CreateAddressableTexture("t1", group, 256); AssetDatabase.SaveAssets(); BuildLayout layout = BuildAndExtractLayout(); // Test Assert.IsTrue(layout.Groups[0].Bundles[0].Files[0].Assets[0].StreamedSize != 0); BuildLayout.SubFile f = layout.Groups[0].Bundles[0].Files[0].SubFiles.First(x => x.Name.EndsWith(".resS")); Assert.IsFalse(f.IsSerializedFile); } finally // cleanup { if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeleteTexture("t1"); } } [Test] public void WhenAllContentsOfAnAssetAreStripped_ExplicitAssetHasNoObjects() { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(ProjectConfigData.BuildLayoutReportFileFormat); AddressableAssetGroup group = null; string assetPath = $"{TempPath}/testpreset.preset"; try { // setup Material obj = new Material(Shader.Find("Transparent/Diffuse")); Preset myPreset = new Preset(obj); AssetDatabase.CreateAsset(myPreset, assetPath); GameObject.DestroyImmediate(obj); AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); string guid = AssetDatabase.AssetPathToGUID(assetPath); group = CreateGroup("Group1"); MakeAddressable(group, guid); AssetDatabase.SaveAssets(); BuildLayout layout = BuildAndExtractLayout(); // Test Assert.AreEqual(0, layout.Groups[0].Bundles[0].Files[0].Assets[0].SerializedSize); } finally // cleanup { if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); AssetDatabase.DeleteAsset(assetPath); } } class SpritePackerScope : IDisposable { SpritePackerMode m_PrevMode; public SpritePackerScope(SpritePackerMode mode) { m_PrevMode = EditorSettings.spritePackerMode; EditorSettings.spritePackerMode = mode; } public void Dispose() { EditorSettings.spritePackerMode = m_PrevMode; } } [Test] public void WhenReferencedObjectIdentifiedWithFilename_ObjectRepresentedInDataFromOtherAssets() { using (new SpritePackerScope(SpritePackerMode.BuildTimeOnlyAtlas)) { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(ProjectConfigData.BuildLayoutReportFileFormat); AddressableAssetGroup group = null; try { // setup BuildCache.PurgeCache(false); group = CreateGroup("Group1"); string textureGUID = CreateSpriteTexture("spritetexture", 256, false); MakeAddressable(group, CreateSpriteAtlas("atlas", textureGUID)); AssetDatabase.SaveAssets(); BuildLayout layout = BuildAndExtractLayout(); // Test BuildLayout.DataFromOtherAsset otherAssets = layout.Groups[0].Bundles[0].Files[0].Assets[0].InternalReferencedOtherAssets[0]; Assert.AreEqual(2, layout.Groups[0].Bundles[0].Files[0].Assets[0].InternalReferencedOtherAssets.Count); CollectionAssert.Contains(otherAssets.ReferencingAssets, layout.Groups[0].Bundles[0].Files[0].Assets[0]); } finally // cleanup { if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeleteSpriteAtlas("atlas"); DeleteTexture("spritetexture"); } } } [Test] public void WhenBuildRemoteCatalogIsDisabled_BuildLayoutContainsCatalogHash() { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(ProjectConfigData.BuildLayoutReportFileFormat); AddressableAssetGroup group = null; bool prevBuildRemoteCatalog = Settings.BuildRemoteCatalog; try { // setup group = CreateGroup("Group1"); CreateAddressablePrefab("p1", group); AssetDatabase.SaveAssets(); BuildLayout layout = BuildAndExtractLayout(); // Test Assert.IsFalse(string.IsNullOrEmpty(layout.AddressablesRuntimeSettings.CatalogHash), "Catalog Hash was not correctly written to the Layout"); Assert.AreEqual(32, layout.AddressablesRuntimeSettings.CatalogHash.Length, "Catalog Hash was not correctly written to the Layout, incorrect size for hash"); Assert.AreEqual(32, layout.BuildResultHash.Length, "Build is expected to have a result hash for the build"); } finally // cleanup { Settings.BuildRemoteCatalog = prevBuildRemoteCatalog; if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeletePrefab("p1"); } } [Test] public void WhenBuildContainsMonoScripts_LayoutDoesNotHaveReferencesToMonoScriptAssets() { string layoutFilePath = BuildLayoutGenerationTask.GetLayoutFilePathForFormat(ProjectConfigData.BuildLayoutReportFileFormat); AddressableAssetGroup group = null; bool prevBuildRemoteCatalog = Settings.BuildRemoteCatalog; try { // setup group = CreateGroup("Group1"); CreateAddressableScriptableObject("so1", group); AssetDatabase.SaveAssets(); BuildLayout layout = BuildAndExtractLayout(); // Test foreach (BuildLayout.ExplicitAsset explicitAsset in BuildLayoutHelpers.EnumerateAssets(layout)) { foreach (var referencedAsset in explicitAsset.InternalReferencedExplicitAssets) { Assert.IsNotNull(referencedAsset, "Referenced Asset was null, this was likely a stripped MonoScript"); Assert.IsTrue(!referencedAsset.AssetPath.EndsWith(".cs") && referencedAsset.AssetPath.EndsWith(".dll")); } foreach (var referencedAsset in explicitAsset.ExternallyReferencedAssets) { Assert.IsNotNull(referencedAsset, "Referenced Asset was null, this was likely a stripped MonoScript"); Assert.IsTrue(!referencedAsset.AssetPath.EndsWith(".cs") && referencedAsset.AssetPath.EndsWith(".dll")); } foreach (var referencedAsset in explicitAsset.InternalReferencedOtherAssets) { Assert.IsNotNull(referencedAsset, "Referenced Asset was null, this was likely a stripped MonoScript"); Assert.IsTrue(!referencedAsset.AssetPath.EndsWith(".cs") && referencedAsset.AssetPath.EndsWith(".dll")); } } } finally // cleanup { Settings.BuildRemoteCatalog = prevBuildRemoteCatalog; if (group != null) Settings.RemoveGroup(group); if (File.Exists(layoutFilePath)) File.Delete(layoutFilePath); DeleteScriptableObject("so1"); } } }