initial commit

This commit is contained in:
Jo 2025-01-07 02:06:59 +01:00
parent 6715289efe
commit 788c3389af
37645 changed files with 2526849 additions and 80 deletions

View file

@ -0,0 +1,115 @@
#if UNITY_2022_2_OR_NEWER
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Unity.Collections.LowLevel.Unsafe;
using Unity.IO.LowLevel.Unsafe;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests.ContentLoad
{
[Serializable()]
public class Catalog
{
[Serializable()]
public class ContentFileInfo
{
public string Filename;
public List<string> Dependencies;
}
[Serializable()]
public class AddressableLocation
{
public string AddressableName;
public string Filename;
public ulong LFID;
}
public List<ContentFileInfo> ContentFiles = new List<ContentFileInfo>();
public List<AddressableLocation> Locations = new List<AddressableLocation>();
private Dictionary<string, AddressableLocation> AddressToLocation =
new Dictionary<string, AddressableLocation>();
private Dictionary<string, ContentFileInfo> FileToInfo = new Dictionary<string, ContentFileInfo>();
public Catalog()
{
}
unsafe public static string ReadAllTextVFS(string path)
{
FileInfoResult infoResult;
ReadHandle h = AsyncReadManager.GetFileInfo(path, &infoResult);
h.JobHandle.Complete();
var getInfoStatus = h.Status;
h.Dispose();
if (getInfoStatus != ReadStatus.Complete)
throw new Exception($"Could not get file info for path {path}");
FileHandle fH = AsyncReadManager.OpenFileAsync(path);
ReadCommand cmd;
cmd.Buffer = UnsafeUtility.Malloc(infoResult.FileSize, 0, Unity.Collections.Allocator.Temp);
cmd.Offset = 0;
cmd.Size = infoResult.FileSize;
var readHandle = AsyncReadManager.Read(path, &cmd, 1);
readHandle.JobHandle.Complete();
AsyncReadManager.CloseCachedFileAsync(path).Complete();
var readResult = readHandle.Status;
readHandle.Dispose();
if (readResult != ReadStatus.Complete)
{
UnsafeUtility.Free(cmd.Buffer, Unity.Collections.Allocator.Temp);
throw new Exception($"Failed to read data from {path}");
}
// Convert to string
string text = System.Text.Encoding.Default.GetString((byte*) cmd.Buffer, (int) cmd.Size);
UnsafeUtility.Free(cmd.Buffer, Unity.Collections.Allocator.Temp);
return text;
}
public static Catalog LoadFromFile(string path)
{
string jsonText = ReadAllTextVFS(path);
Catalog catalog = JsonUtility.FromJson<Catalog>(jsonText);
catalog.OnDeserialize();
return catalog;
}
public AddressableLocation GetLocation(string name)
{
return AddressToLocation[name];
}
public ContentFileInfo GetFileInfo(string filename)
{
return FileToInfo[filename];
}
void BuildMaps()
{
AddressToLocation = new Dictionary<string, AddressableLocation>();
FileToInfo = new Dictionary<string, ContentFileInfo>();
foreach (ContentFileInfo f in ContentFiles)
FileToInfo[f.Filename] = f;
foreach (AddressableLocation l in Locations)
{
AddressToLocation[l.AddressableName] = l;
}
}
[OnDeserializing()]
public void OnDeserialize()
{
BuildMaps();
}
}
}
#endif

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cd104a158e673469cab9e0947990aef2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,201 @@
#if UNITY_2022_2_OR_NEWER
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using Unity.Collections;
using Unity.Content;
using Unity.Loading;
using UnityEngine;
using UnityEngine.TestTools;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Utilities;
#endif
namespace UnityEditor.Build.Pipeline.Tests.ContentLoad
{
public abstract class ContentFileFixture : IPrebuildSetup
{
protected string m_ContentRoot;
protected string ContentRoot
{
get { return m_ContentRoot; }
}
public virtual string ContentDir
{
get { return Path.Combine(ContentRoot, this.GetType().Name); }
}
protected Catalog m_Catalog;
protected string m_CatalogDir;
protected ContentNamespace m_NS;
Dictionary<string, ContentFile> m_LoadedFiles = new Dictionary<string, ContentFile>();
List<ContentFile> m_ToUnload = new List<ContentFile>();
public ContentFileFixture()
{
m_ContentRoot = Application.streamingAssetsPath;
}
[SetUp]
public void SetUp()
{
m_NS = ContentNamespace.GetOrCreateNamespace("Test");
}
[TearDown]
public void TearDown()
{
for (int i = m_ToUnload.Count - 1; i >= 0; i--)
m_ToUnload[i].UnloadAsync().WaitForCompletion(0);
m_ToUnload.Clear();
if (TestContext.CurrentContext.Result.Outcome != ResultState.Failure)
{
Assert.AreEqual(0, ContentLoadInterface.GetContentFiles(m_NS).Length);
Assert.AreEqual(0, ContentLoadInterface.GetSceneFiles(m_NS).Length);
}
m_NS.Delete();
}
public void LoadCatalog(string name, bool mountAllArchives = true)
{
m_CatalogDir = Path.Combine(ContentDir, name);
string path = Path.Combine(m_CatalogDir, "catalog.json");
m_Catalog = Catalog.LoadFromFile(path);
}
public string GetVFSFilename(string fn)
{
return Path.Combine(m_CatalogDir, fn);
}
#if UNITY_EDITOR
protected abstract void PrepareBuildLayout();
Dictionary<string, List<AssetBundleBuild>> m_CurrentBuildLayouts =
new Dictionary<string, List<AssetBundleBuild>>();
protected virtual void DefaultBuild()
{
PrepareBuildLayout();
foreach (var kvp in m_CurrentBuildLayouts)
{
string outDir = Path.Combine(ContentDir, kvp.Key);
BuildContentLoadsAndCatalog(outDir, kvp.Value.ToArray());
}
// Create a file manifest
List<string> allFiles = Directory.EnumerateFiles(ContentDir, "*", SearchOption.AllDirectories)
.Select(x => x.Substring(ContentDir.Length + 1))
.Where(x => !x.EndsWith(".meta") && x != "FileManifest.txt").ToList();
string manifestDst = Path.Combine(ContentDir, "FileManifest.txt");
File.WriteAllText(manifestDst, string.Join('\n', allFiles));
}
public class ContentBuildCatalog : IDisposable
{
List<AssetBundleBuild> m_Items = new List<AssetBundleBuild>();
ContentFileFixture m_Fixture;
string m_Name;
public ContentBuildCatalog(string name, ContentFileFixture fixture)
{
m_Name = name;
m_Fixture = fixture;
}
public void Add(AssetBundleBuild build)
{
m_Items.Add(build);
}
public void Dispose()
{
m_Fixture.m_CurrentBuildLayouts.Add(m_Name, m_Items);
}
}
public ContentBuildCatalog CreateCatalog(string name)
{
return new ContentBuildCatalog(name, this);
}
protected void BuildContentLoadsAndCatalog(string outdir, AssetBundleBuild[] bundles)
{
Directory.CreateDirectory(outdir);
for (int i = 0; i < bundles.Length; i++)
bundles[i].assetBundleName = "";
var content = new BundleBuildContent(bundles);
var target = EditorUserBuildSettings.activeBuildTarget;
var group = BuildPipeline.GetBuildTargetGroup(target);
var parameters = new BundleBuildParameters(target, group, outdir);
parameters.UseCache = false;
parameters.BundleCompression = UnityEngine.BuildCompression.Uncompressed;
parameters.NonRecursiveDependencies = false;
parameters.WriteLinkXML = true;
var taskList = DefaultBuildTasks.ContentFileCompatible();
ClusterOutput cOutput = new ClusterOutput();
IBundleBuildResults results;
ContentPipeline.BuildAssetBundles(parameters, content, out results, taskList,
new ContentFileIdentifiers(), cOutput);
Catalog catalog = new Catalog();
foreach (var result in results.WriteResults)
{
var fi = new Catalog.ContentFileInfo();
fi.Filename = result.Key;
fi.Dependencies = new List<string>();
foreach (var extRef in result.Value.externalFileReferences)
fi.Dependencies.Add(extRef.filePath);
catalog.ContentFiles.Add(fi);
}
foreach (AssetBundleBuild ab in bundles)
{
for (int i = 0; i < ab.assetNames.Length; i++)
{
string assetName = ab.assetNames[i];
GUID guid = AssetDatabase.GUIDFromAssetPath(assetName);
var loc = new Catalog.AddressableLocation();
loc.AddressableName = ab.addressableNames[i];
if (assetName.EndsWith(".unity"))
{
loc.Filename = guid.ToString();
loc.LFID = 0;
}
else
{
ObjectIdentifier oi =
ContentBuildInterface.GetPlayerObjectIdentifiersInAsset(guid,
EditorUserBuildSettings.activeBuildTarget)[0];
loc.Filename = cOutput.ObjectToCluster[oi].ToString();
loc.LFID = (ulong) cOutput.ObjectToLocalID[oi];
}
catalog.Locations.Add(loc);
}
}
File.WriteAllText(Path.Combine(outdir, "catalog.json"), JsonUtility.ToJson(catalog));
}
#endif
public void Setup()
{
#if UNITY_EDITOR
DefaultBuild();
#endif
}
}
}
#endif

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b827df2c3769047c0a5c00f7654e75a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,177 @@
#if UNITY_2022_2_OR_NEWER
using System;
using NUnit.Framework;
using System.Collections;
using System.IO;
using System.Linq;
using Unity.Collections;
using Unity.Content;
using Unity.IO.Archive;
using Unity.Loading;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
using UnityEditor.TestTools;
#endif
namespace UnityEditor.Build.Pipeline.Tests.ContentLoad
{
abstract public class SceneTests : ContentFileFixture
{
public static IEnumerator UnloadAllScenesExceptInitTestSceneAsync()
{
#pragma warning disable 0618
var allScenes = SceneManager.GetAllScenes();
var allScenesNoInit = allScenes.Where(x => !x.name.Contains("InitTestScene")).ToList();
if (allScenes.Length == allScenesNoInit.Count)
SceneManager.CreateScene("InitTestScene");
foreach (var allScene in allScenesNoInit)
{
yield return SceneManager.UnloadSceneAsync(allScene);
}
#pragma warning restore 0618
}
[UnitySetUp]
public IEnumerator UnloadAllScenesExceptInitTestScene()
{
Assert.AreEqual(1, SceneManager.sceneCount);
yield return null;
yield return UnloadAllScenesExceptInitTestSceneAsync();
}
[TearDown]
public void Teardown()
{
}
public ContentSceneFile LoadSceneHelper(string path, string sceneName, LoadSceneMode mode, ContentFile[] deps,
bool integrate = true, bool autoIntegrate = false)
{
var sceneParams = new ContentSceneParameters();
sceneParams.loadSceneMode = mode;
sceneParams.localPhysicsMode = LocalPhysicsMode.None;
sceneParams.autoIntegrate = autoIntegrate;
NativeArray<ContentFile> files =
new NativeArray<ContentFile>(deps.Length, Allocator.Temp, NativeArrayOptions.ClearMemory);
for (int i = 0; i < deps.Length; i++)
{
files[i] = deps[i];
}
ContentSceneFile op = ContentLoadInterface.LoadSceneAsync(m_NS, path, sceneName, sceneParams, files);
files.Dispose();
if (integrate)
{
op.WaitForLoadCompletion(0);
if (op.Status == SceneLoadingStatus.WaitingForIntegrate)
op.IntegrateAtEndOfFrame();
}
return op;
}
private void AssertNoDepSceneLoaded(ContentSceneFile sceneFile)
{
LoadCatalog("nodepscene");
Assert.AreEqual(SceneLoadingStatus.Complete, sceneFile.Status);
Scene scene = sceneFile.Scene;
GameObject[] objs = scene.GetRootGameObjects();
GameObject test = objs.First(x => x.name == "testobject");
Assert.IsTrue(SceneManager.GetSceneByName("testscene").IsValid());
Assert.AreEqual(sceneFile, ContentLoadInterface.GetSceneFiles(m_NS)[0]);
}
[UnityTest]
public IEnumerator CanLoadSceneWithNoDependencies()
{
LoadCatalog("nodepscene");
Catalog.AddressableLocation p1Loc = m_Catalog.GetLocation("nodepscene");
ArchiveHandle aHandle = ArchiveFileInterface.MountAsync(ContentNamespace.Default, GetVFSFilename(p1Loc.Filename), "a:");
aHandle.JobHandle.Complete();
Assert.True(aHandle.JobHandle.IsCompleted);
Assert.True(aHandle.Status == ArchiveStatus.Complete);
try
{
var mountPath = aHandle.GetMountPath();
var vfsPath = Path.Combine(mountPath, p1Loc.Filename);
var sceneFile = LoadSceneHelper(vfsPath, "testscene", LoadSceneMode.Additive,
new ContentFile[] {ContentFile.GlobalTableDependency});
while (sceneFile.Status == SceneLoadingStatus.InProgress)
yield return null;
Assert.AreEqual(SceneLoadingStatus.WillIntegrateNextFrame, sceneFile.Status);
yield return null;
AssertNoDepSceneLoaded(sceneFile);
sceneFile.UnloadAtEndOfFrame();
yield return null;
}
finally
{
aHandle.Unmount();
}
}
#if UNITY_EDITOR
protected override void PrepareBuildLayout()
{
Directory.CreateDirectory("Assets/Temp");
// Create a scene with no dependencies
using (var c = CreateCatalog("nodepscene"))
{
Scene scene1 = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
RenderSettings.skybox = null;
SceneManager.SetActiveScene(scene1);
var go = new GameObject("testobject");
EditorSceneManager.SaveScene(scene1, "Assets/Temp/nodepscene.unity");
EditorSceneManager.CloseScene(scene1, true);
c.Add(
new AssetBundleBuild
{
assetNames = new string[] {"Assets/Temp/nodepscene.unity"},
addressableNames = new string[] {"nodepscene"}
});
}
}
#endif
}
[UnityPlatform(exclude = new RuntimePlatform[]
{RuntimePlatform.LinuxEditor, RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor})]
class SceneTests_Standalone : SceneTests
{
}
#if UNITY_EDITOR
[UnityPlatform(RuntimePlatform.WindowsEditor)]
[RequirePlatformSupport(BuildTarget.StandaloneWindows64)]
class SceneTests_WindowsEditor : SceneTests
{
}
[UnityPlatform(RuntimePlatform.OSXEditor)]
[RequirePlatformSupport(BuildTarget.StandaloneOSX)]
class SceneTests_OSXEditor : SceneTests
{
}
[UnityPlatform(RuntimePlatform.LinuxEditor)]
[RequirePlatformSupport(BuildTarget.StandaloneLinux64)]
class SceneTests_LinuxEditor : SceneTests
{
}
#endif
}
#endif

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5a22dad5548343f7b41b6f993d78696
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,109 @@
#if UNITY_2022_2_OR_NEWER
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Unity.Loading;
using Unity.Collections;
using Unity.Content;
using System.IO;
using Unity.IO.Archive;
#if UNITY_EDITOR
using UnityEditor.TestTools;
#endif
namespace UnityEditor.Build.Pipeline.Tests.ContentLoad
{
abstract public class ContentFileTests : ContentFileFixture
{
const string kCatalogTextData = "TextData";
[UnityTest]
public IEnumerator LoadFileAsync_CanLoadText()
{
LoadCatalog(kCatalogTextData);
Catalog.AddressableLocation p1Loc = m_Catalog.GetLocation("Text");
Catalog.ContentFileInfo fInfo = m_Catalog.GetFileInfo(p1Loc.Filename);
ArchiveHandle aHandle = ArchiveFileInterface.MountAsync(ContentNamespace.Default, GetVFSFilename(fInfo.Filename), "a:");
aHandle.JobHandle.Complete();
Assert.True(aHandle.JobHandle.IsCompleted);
Assert.True(aHandle.Status == ArchiveStatus.Complete);
try
{
var mountPath = aHandle.GetMountPath();
var vfsPath = Path.Combine(mountPath, fInfo.Filename);
ContentFile fileHandle = ContentLoadInterface.LoadContentFileAsync(m_NS, vfsPath, new NativeArray<ContentFile>());
while (fileHandle.LoadingStatus == LoadingStatus.InProgress)
yield return null;
TextAsset text = (TextAsset)fileHandle.GetObject(p1Loc.LFID);
Assert.NotZero(text.bytes.Length);
fileHandle.UnloadAsync().WaitForCompletion(0);
}
finally
{
aHandle.Unmount();
}
}
#if UNITY_EDITOR
private static System.Int32 randomSeed = 1;
protected override void PrepareBuildLayout()
{
Directory.CreateDirectory("Assets/Temp");
using (var c = CreateCatalog(kCatalogTextData))
{
var path = AssetDatabase.GenerateUniqueAssetPath("Assets/Temp/textfile.txt");
using (var fs = File.Create(path))
{
var bytes = new byte[64 * 1024];
var rand = new System.Random(++randomSeed);
rand.NextBytes(bytes);
for (System.UInt32 j = 0; j < 10; j++)
fs.Write(bytes, 0, bytes.Length);
}
AssetDatabase.ImportAsset(path);
c.Add(new AssetBundleBuild()
{
assetNames = new string[] { path },
addressableNames = new string[] { "Text" }
});
}
}
#endif
}
[UnityPlatform(exclude = new RuntimePlatform[]
{RuntimePlatform.LinuxEditor, RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor})]
class ContentFileTests_Standalone : ContentFileTests
{
}
#if UNITY_EDITOR
[UnityPlatform(RuntimePlatform.WindowsEditor)]
[RequirePlatformSupport(BuildTarget.StandaloneWindows64)]
class ContentFileTests_WindowsEditor : ContentFileTests
{
}
[UnityPlatform(RuntimePlatform.OSXEditor)]
[RequirePlatformSupport(BuildTarget.StandaloneOSX)]
class ContentFileTests_OSXEditor : ContentFileTests
{
}
[UnityPlatform(RuntimePlatform.LinuxEditor)]
[RequirePlatformSupport(BuildTarget.StandaloneLinux64)]
class ContentFileTests_LinuxEditor : ContentFileTests
{
}
#endif
}
#endif

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c951970d3a8774823adda08337ad175b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d759189bc3e47411985027b7cb98f986
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,213 @@
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using UnityEditor;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Pipeline.Tests;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
public class ArchiveAndCompressTestFixture
{
internal static void AssertDirectoriesEqual(string expectedDirectory, string directory, int expectedCount = -1)
{
string[] expectedFiles = Directory.GetFiles(expectedDirectory);
Array.Sort(expectedFiles);
string[] files = Directory.GetFiles(directory);
Array.Sort(files);
if (expectedCount != -1)
Assert.AreEqual(expectedCount, expectedFiles.Length);
Assert.AreEqual(expectedFiles.Length, files.Length);
for (int i = 0; i < expectedFiles.Length; i++)
FileAssert.AreEqual(expectedFiles[i], files[i]);
}
internal string CreateTempDir(string testDir)
{
string tempDirectory = Path.Combine("Temp", testDir);
Directory.CreateDirectory(tempDirectory);
return tempDirectory;
}
internal static void WriteRandomData(string filename, long size, int seed)
{
System.Random r = new System.Random(seed);
using (var s = File.Open(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
long written = 0;
byte[] bytes = new byte[Math.Min(1 * 1024 * 1024, size)];
while (written < size)
{
r.NextBytes(bytes);
int writeSize = (int)Math.Min(size - written, bytes.Length);
s.Write(bytes, 0, writeSize);
written += bytes.Length;
}
}
}
internal string CreateFileOfSize(string path, long size)
{
System.Random r = new System.Random(m_Seed);
m_Seed++;
Directory.CreateDirectory(Path.GetDirectoryName(path));
WriteRandomData(path, size, 0);
return path;
}
int m_Seed;
internal string m_TestTempDir;
internal string m_FixtureTempDir;
[OneTimeSetUp]
public void OneTimeSetup()
{
// Spooky hash has a static constructor that must be called on the main thread.
m_FixtureTempDir = CreateTempDir("FixtureTemp");
HashingMethods.CalculateStream(new MemoryStream(new byte[] { 1 }));
}
[OneTimeTearDown]
public void OneTimeTeardown()
{
Directory.Delete(m_FixtureTempDir, true);
}
[SetUp]
public void Setup()
{
m_Seed = 0;
BuildCache.PurgeCache(false); // TOOD: If the build cache didn't use global directories, this wouldn't be necessary
m_TestTempDir = CreateTempDir("TestTemp");
m_SizeCounts = new Dictionary<int, int>();
}
[TearDown]
public void Teardown()
{
if (Directory.Exists(m_TestTempDir))
Directory.Delete(m_TestTempDir, true);
}
internal string[] RunWebExtract(string filePath)
{
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 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);
//UnityEngine.Debug.Log(output);
return Directory.GetFiles(filePath + "_data");
}
internal ArchiveAndCompressBundles.TaskInput GetDefaultInput()
{
ArchiveAndCompressBundles.TaskInput input = new ArchiveAndCompressBundles.TaskInput();
input.InternalFilenameToWriteResults = new Dictionary<string, WriteResult>();
#if UNITY_2019_3_OR_NEWER
input.BundleNameToAdditionalFiles = new Dictionary<string, List<ResourceFile>>();
#endif
input.InternalFilenameToBundleName = new Dictionary<string, string>();
input.AssetToFilesDependencies = new Dictionary<UnityEditor.GUID, List<string>>();
input.InternalFilenameToWriteMetaData = new Dictionary<string, SerializedFileMetaData>();
input.BuildCache = null;
input.Threaded = false;
input.ProgressTracker = null;
input.OutCachedBundles = new List<string>();
input.GetCompressionForIdentifier = (x) => UnityEngine.BuildCompression.LZ4;
input.GetOutputFilePathForIdentifier = (x) => Path.Combine(m_TestTempDir, "bundleoutdir", x);
input.TempOutputFolder = Path.Combine(m_TestTempDir, "temptestdir");
return input;
}
internal WriteResult AddSimpleBundle(ArchiveAndCompressBundles.TaskInput input, string bundleName, string internalName, string filePath)
{
WriteResult writeResult = new WriteResult();
ResourceFile file = new ResourceFile();
file.SetFileName(filePath);
file.SetFileAlias(internalName);
file.SetSerializedFile(false);
writeResult.SetResourceFiles(new ResourceFile[] { file });
input.InternalFilenameToWriteResults.Add(internalName, writeResult);
input.InternalFilenameToBundleName.Add(internalName, bundleName);
SerializedFileMetaData md = new SerializedFileMetaData();
md.RawFileHash = HashingMethods.CalculateFile(filePath).ToHash128();
md.ContentHash = HashingMethods.CalculateFile(filePath).ToHash128();
input.InternalFilenameToWriteMetaData.Add(internalName, md);
return writeResult;
}
#if UNITY_2019_3_OR_NEWER
internal void AddRawFileThatTargetsBundle(ArchiveAndCompressBundles.TaskInput input, string targetBundleName, string rawFileInternalName, string filePath)
{
ResourceFile file = new ResourceFile();
file.SetFileName(filePath);
file.SetFileAlias(rawFileInternalName);
file.SetSerializedFile(false);
List<ResourceFile> files = new List<ResourceFile> { file };
input.BundleNameToAdditionalFiles.Add(targetBundleName, files);
}
internal void AddRawFileThatTargetsBundle(ArchiveAndCompressBundles.TaskInput input, string targetBundleName, string rawFileInternalName)
{
string tempFilename = CreateFileOfSize(GetUniqueFilename(Path.Combine(m_FixtureTempDir, "src", "rawfile.bin")), 1024);
AddRawFileThatTargetsBundle(input, targetBundleName, rawFileInternalName, tempFilename);
}
#endif
public static string GetUniqueFilename(string desiredFilename)
{
string dir = Path.GetDirectoryName(desiredFilename);
string noExtention = Path.GetFileNameWithoutExtension(desiredFilename);
string ext = Path.GetExtension(desiredFilename);
for (int i = 0; true; i++)
{
string testName = Path.Combine(dir, Path.Combine($"{noExtention}{i}{ext}"));
if (!File.Exists(testName))
{
return testName;
}
}
}
internal Dictionary<int, int> m_SizeCounts = new Dictionary<int, int>();
internal WriteResult AddSimpleBundle(ArchiveAndCompressBundles.TaskInput input, string bundleName, string internalName, int size = 1024)
{
if (!m_SizeCounts.ContainsKey(size))
m_SizeCounts[size] = 0;
int count = m_SizeCounts[size]++;
string tempFilename = Path.Combine(m_FixtureTempDir, "src", $"testfile_{size}_{count}.bin");
if (!File.Exists(tempFilename))
CreateFileOfSize(tempFilename, size);
return AddSimpleBundle(input, bundleName, internalName, tempFilename);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ab8ec987142fec4f84a3daa06a8b4c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,458 @@
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
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 UnityEngine;
using UnityEngine.TestTools;
namespace UnityEditor.Build.Pipeline.Tests
{
public class ArchiveAndCompressTests : ArchiveAndCompressTestFixture
{
[Test]
public void WhenAssetInBundleHasDependencies_DependenciesAreInDetails()
{
ArchiveAndCompressBundles.TaskInput input = GetDefaultInput();
AddSimpleBundle(input, "mybundle", "internalName");
AddSimpleBundle(input, "mybundle2", "internalName2");
AddSimpleBundle(input, "mybundle3", "internalName3");
input.AssetToFilesDependencies.Add(new GUID(), new List<string>() { "internalName", "internalName2" });
input.AssetToFilesDependencies.Add(GUID.Generate(), new List<string>() { "internalName", "internalName3" });
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output);
Assert.AreEqual(2, output.BundleDetails["mybundle"].Dependencies.Length);
Assert.AreEqual("mybundle2", output.BundleDetails["mybundle"].Dependencies[0]);
Assert.AreEqual("mybundle3", output.BundleDetails["mybundle"].Dependencies[1]);
}
[Test]
public void WhenBundleDoesNotHaveDependencies_DependenciesAreNotInDetails()
{
ArchiveAndCompressBundles.TaskInput input = GetDefaultInput();
AddSimpleBundle(input, "mybundle", "internalName");
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output);
Assert.AreEqual(0, output.BundleDetails["mybundle"].Dependencies.Length);
}
[TestCase("01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_d", ".")]
[TestCase("C:/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars", ".")]
[TestCase(".", "01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_d")]
[TestCase(".", "C:/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars/01_long_directory_path_for_35chars")]
[UnityPlatform(exclude = new[] { RuntimePlatform.LinuxEditor, RuntimePlatform.OSXEditor })]
public void WhenUsingLongPath_CopyFileWithTimestampIfDifferent_ThrowsPathTooLongException(string path1, string path2)
{
Assert.Throws<PathTooLongException>(() => ArchiveAndCompressBundles.CopyFileWithTimestampIfDifferent(path1, path2, null));
}
[Test]
// Windows has Unicode path notation for long paths: \\?\
// however this does not work on all unity editor versions or windows version.
// notably this fails on Yamato 2019.4 through 2021.1, but passed on trunk (on Nov 4, 2021)
[UnityPlatform(exclude = new[] { RuntimePlatform.WindowsEditor })]
public void PlatformCanHandle_LongPathReadingAndWriting()
{
string root = Path.GetFullPath("FolderDepthTest");
string fullPath = root;
while (fullPath.Length < 300)
fullPath = Path.Combine(fullPath, $"IncreaseFolderDepth_{fullPath.Length}");
string file1 = Path.Combine(fullPath, "test1.txt");
string file2 = Path.Combine(fullPath, "test2.txt");
Assert.DoesNotThrow(() =>
{
// Can create folder > 260 characters deep
Directory.CreateDirectory(fullPath);
// Can write file > 260 characters deep
File.WriteAllText(file1, "Test file contents");
// Can move file > 260 characters deep
File.Move(file1, file2);
// Can read file > 260 characters deep
var contents = File.ReadAllText(file2);
Assert.AreEqual("Test file contents", contents);
// Can delete file > 260 characters deep
File.Delete(file2);
// Can delete folder > 260 characters deep
Directory.Delete(fullPath);
});
// Cleanup
Directory.Delete(root, true);
}
public class RebuildTestContext
{
internal ArchiveAndCompressBundles.TaskInput input;
internal ArchiveAndCompressTests _this;
};
public static IEnumerable RebuildTestCases
{
get
{
yield return new TestCaseData(false, new Action<RebuildTestContext>((ctx) => { })).SetName("NoChanges");
yield return new TestCaseData(true, new Action<RebuildTestContext>((ctx) =>
{
ctx.input.InternalFilenameToWriteMetaData["internalName"] = new SerializedFileMetaData() { ContentHash = new Hash128(0, 1), RawFileHash = new Hash128(1, 2) };
})).SetName("SourceFileHashChanges");
yield return new TestCaseData(true, new Action<RebuildTestContext>((ctx) =>
{
ctx.input.GetCompressionForIdentifier = (x) => UnityEngine.BuildCompression.Uncompressed;
})).SetName("CompressionChanges");
#if UNITY_2019_3_OR_NEWER
yield return new TestCaseData(true, new Action<RebuildTestContext>((ctx) =>
{
ctx._this.AddRawFileThatTargetsBundle(ctx.input, "mybundle", "rawInternalName");
})).SetName("AddAdditionalFile");
#endif
}
}
[Test, TestCaseSource(typeof(ArchiveAndCompressTests), "RebuildTestCases")]
public void WhenInputsChanges_AndRebuilt_CachedDataIsUsedAsExpected(bool shouldRebuild, Action<RebuildTestContext> postFirstBuildAction)
{
BuildCache.PurgeCache(false);
using (BuildCache cache = new BuildCache())
{
RebuildTestContext ctx = new RebuildTestContext();
ctx.input = GetDefaultInput();
ctx._this = this;
ctx.input.BuildCache = cache;
AddSimpleBundle(ctx.input, "mybundle", "internalName");
ArchiveAndCompressBundles.Run(ctx.input, out ArchiveAndCompressBundles.TaskOutput output);
cache.SyncPendingSaves();
Assert.AreEqual(0, ctx.input.OutCachedBundles.Count);
postFirstBuildAction(ctx);
ctx.input.OutCachedBundles.Clear();
ArchiveAndCompressBundles.Run(ctx.input, out ArchiveAndCompressBundles.TaskOutput outputReRun);
if (shouldRebuild)
Assert.AreEqual(0, ctx.input.OutCachedBundles.Count);
else
Assert.AreEqual(1, ctx.input.OutCachedBundles.Count);
}
}
[Test]
public void WhenArchiveIsAlreadyBuilt_CachedVersionIsUsed()
{
string bundleOutDir1 = Path.Combine(m_TestTempDir, "bundleoutdir1");
string bundleOutDir2 = Path.Combine(m_TestTempDir, "bundleoutdir2");
Directory.CreateDirectory(bundleOutDir1);
Directory.CreateDirectory(bundleOutDir2);
ArchiveAndCompressBundles.TaskInput input = GetDefaultInput();
BuildCache cache = new BuildCache();
input.BuildCache = cache;
AddSimpleBundle(input, "mybundle", "internalName");
input.GetOutputFilePathForIdentifier = (x) => Path.Combine(bundleOutDir1, x);
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output);
Assert.AreEqual(0, input.OutCachedBundles.Count);
cache.SyncPendingSaves();
input.GetOutputFilePathForIdentifier = (x) => Path.Combine(bundleOutDir2, x);
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output2);
Assert.AreEqual(1, input.OutCachedBundles.Count);
Assert.AreEqual("mybundle", input.OutCachedBundles[0]);
AssertDirectoriesEqual(bundleOutDir1, bundleOutDir2, 1);
}
[Test]
public void WhenArchiveIsCached_AndRebuildingArchive_HashIsAssignedToOutput()
{
string bundleName = "mybundle";
ArchiveAndCompressBundles.TaskInput input = GetDefaultInput();
BuildCache cache = new BuildCache();
input.BuildCache = cache;
AddSimpleBundle(input, bundleName, "internalName");
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output);
var hash = output.BundleDetails[bundleName].Hash;
Assert.AreNotEqual(Hash128.Parse(""), output.BundleDetails[bundleName].Hash);
cache.SyncPendingSaves();
//Now our bundle is cached so we'll run again and make sure the hashes are non-zero and equal
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output2);
Assert.AreEqual(hash, output2.BundleDetails[bundleName].Hash);
}
public class ContentHashTestContext
{
internal ArchiveAndCompressBundles.TaskInput input;
internal GUID assetGUID;
internal ArchiveAndCompressTests _this;
};
public static IEnumerable ContentHashTestCases
{
get
{
yield return new TestCaseData(true, new Action<ContentHashTestContext>((ctx) =>
{
ctx.input.AssetToFilesDependencies[ctx.assetGUID] = new List<string>() { "internalName", "internalName3" };
})).SetName("DependencyChanges");
yield return new TestCaseData(true, new Action<ContentHashTestContext>((ctx) =>
{
ctx.input.InternalFilenameToWriteMetaData["internalName"].ContentHash = new Hash128(128, 128);
})).SetName("ContentHashChanges");
yield return new TestCaseData(false, new Action<ContentHashTestContext>((ctx) =>
{
ctx.input.InternalFilenameToWriteMetaData["internalName"].RawFileHash = new Hash128(128, 128);
})).SetName("RawHashChanges");
}
}
[Test, TestCaseSource(typeof(ArchiveAndCompressTests), "ContentHashTestCases")]
public void WhenInputsChange_BundleOutputHashIsAffectedAsExpected(bool hashShouldChange, Action<ContentHashTestContext> postFirstBuildAction)
{
ContentHashTestContext ctx = new ContentHashTestContext();
ctx.input = GetDefaultInput();
WriteResult result = AddSimpleBundle(ctx.input, "mybundle", "internalName");
WriteResult result2 = AddSimpleBundle(ctx.input, "mybundle2", "internalName2");
WriteResult result3 = AddSimpleBundle(ctx.input, "mybundle3", "internalName3");
ctx.assetGUID = GUID.Generate();
ctx.input.AssetToFilesDependencies.Add(ctx.assetGUID, new List<string>() { "internalName", "internalName2" });
ArchiveAndCompressBundles.Run(ctx.input, out ArchiveAndCompressBundles.TaskOutput output);
postFirstBuildAction(ctx);
ArchiveAndCompressBundles.Run(ctx.input, out ArchiveAndCompressBundles.TaskOutput output2);
Hash128 prevHash = output.BundleDetails["mybundle"].Hash;
Hash128 newHash = output2.BundleDetails["mybundle"].Hash;
if (hashShouldChange)
Assert.AreNotEqual(prevHash, newHash);
else
Assert.AreEqual(prevHash, newHash);
}
#if UNITY_2019_3_OR_NEWER
[Test]
public void WhenBuildingManyArchives_ThreadedAndNonThreadedResultsAreIdentical()
{
const int kBundleCount = 100;
ArchiveAndCompressBundles.TaskInput input = GetDefaultInput();
for (int i = 0; i < kBundleCount; i++)
AddSimpleBundle(input, $"mybundle{i}", $"internalName{i}");
input.Threaded = false;
input.GetOutputFilePathForIdentifier = (x) => Path.Combine(m_TestTempDir, "bundleoutdir_nothreading", x);
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output1);
input.Threaded = true;
input.GetOutputFilePathForIdentifier = (x) => Path.Combine(m_TestTempDir, "bundleoutdir_threading", x);
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output2);
AssertDirectoriesEqual(Path.Combine(m_TestTempDir, "bundleoutdir_nothreading"), Path.Combine(m_TestTempDir, "bundleoutdir_threading"), kBundleCount);
}
#endif
// Start is called before the first frame update
[Test]
public void ResourceFilesAreAddedToBundles()
{
ArchiveAndCompressBundles.TaskInput input = GetDefaultInput();
string bundleOutDir = Path.Combine(m_TestTempDir, "bundleoutdir");
AddSimpleBundle(input, "mybundle", "internalName");
string srcFile = input.InternalFilenameToWriteResults["internalName"].resourceFiles[0].fileName;
ReturnCode code = ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output);
Assert.AreEqual(ReturnCode.Success, code);
string[] files = RunWebExtract(Path.Combine(bundleOutDir, "mybundle"));
Assert.AreEqual(1, files.Length);
FileAssert.AreEqual(files[0], srcFile);
}
[Test]
public void WhenBuildingArchive_BuildLogIsPopulated()
{
ArchiveAndCompressBundles.TaskInput input = GetDefaultInput();
var log = new BuildLog();
input.Log = log;
AddSimpleBundle(input, "mybundle", "internalName");
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output);
var node = log.Root.Children.Find((x) => x.Name.StartsWith("ArchiveItems"));
Assert.IsNotNull(node);
}
class ScopeCapturer : IBuildLogger
{
public List<string> Scopes = new List<string>();
public void AddEntry(LogLevel level, string msg)
{
}
public void BeginBuildStep(LogLevel level, string stepName, bool subStepsCanBeThreaded)
{
lock (Scopes)
{
Scopes.Add(stepName);
}
}
public bool ContainsScopeWithSubstring(string subString)
{
return Scopes.Count((x) => x.Contains(subString)) != 0;
}
public void EndBuildStep()
{
}
}
private void AddSimpleBundleAndBuild(out ArchiveAndCompressBundles.TaskInput input, out string bundleBuildDir)
{
ScopeCapturer log1 = new ScopeCapturer();
string bDir = Path.Combine(m_TestTempDir, "bundleoutdir1");
Directory.CreateDirectory(bDir);
input = GetDefaultInput();
BuildCache cache = new BuildCache();
input.BuildCache = cache;
input.Log = log1;
AddSimpleBundle(input, "mybundle", "internalName");
input.GetOutputFilePathForIdentifier = (x) => Path.Combine(bDir, x);
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output);
Assert.AreEqual(0, input.OutCachedBundles.Count);
Assert.IsTrue(log1.ContainsScopeWithSubstring("Copying From Cache"));
cache.SyncPendingSaves();
bundleBuildDir = bDir;
}
[Test]
public void WhenArchiveIsAlreadyBuilt_AndArchiveIsInOutputDirectory_ArchiveIsNotCopied()
{
AddSimpleBundleAndBuild(out ArchiveAndCompressBundles.TaskInput input, out string bundleOutDir1);
ScopeCapturer log2 = new ScopeCapturer();
input.GetOutputFilePathForIdentifier = (x) => Path.Combine(bundleOutDir1, x);
input.Log = log2;
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output2);
Assert.AreEqual(1, input.OutCachedBundles.Count);
Assert.AreEqual("mybundle", input.OutCachedBundles[0]);
Assert.IsFalse(log2.ContainsScopeWithSubstring("Copying From Cache"));
}
[Test]
public void WhenArchiveIsAlreadyBuilt_AndArchiveIsInOutputDirectoryButTimestampMismatch_ArchiveIsCopied()
{
AddSimpleBundleAndBuild(out ArchiveAndCompressBundles.TaskInput input, out string bundleOutDir1);
// Change the creation timestamp on the bundles
string bundlePath = Path.Combine(bundleOutDir1, "mybundle");
File.SetLastWriteTime(bundlePath, new DateTime(2019, 1, 1));
ScopeCapturer log2 = new ScopeCapturer();
input.GetOutputFilePathForIdentifier = (x) => Path.Combine(bundleOutDir1, x);
input.Log = log2;
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output2);
Assert.AreEqual(1, input.OutCachedBundles.Count);
Assert.AreEqual("mybundle", input.OutCachedBundles[0]);
Assert.IsTrue(log2.ContainsScopeWithSubstring("Copying From Cache"));
}
#if UNITY_2019_3_OR_NEWER
[Test]
public void CanAddRawFilesToBundles()
{
ArchiveAndCompressBundles.TaskInput input = GetDefaultInput();
AddSimpleBundle(input, "mybundle", "internalName");
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output);
AddRawFileThatTargetsBundle(input, "mybundle", "rawName");
ArchiveAndCompressBundles.Run(input, out ArchiveAndCompressBundles.TaskOutput output2);
Assert.IsTrue(output.BundleDetails["mybundle"].Hash.isValid);
Assert.IsTrue(output2.BundleDetails["mybundle"].Hash.isValid);
Assert.AreNotEqual(output.BundleDetails["mybundle"].Hash, output2.BundleDetails["mybundle"].Hash);
}
[Test]
public void SupportsMultiThreadedArchiving_WhenEditorIs20193OrLater_IsTrue()
{
Assert.IsTrue(ReflectionExtensions.SupportsMultiThreadedArchiving);
}
#else
[Test]
public void SupportsMultiThreadedArchiving_WhenEditorIsBefore20193_IsFalse()
{
Assert.IsFalse(ReflectionExtensions.SupportsMultiThreadedArchiving);
}
#endif
[Test]
public void CalculateBundleDependencies_ReturnsRecursiveDependencies_ForNonRecursiveInputs()
{
// Dictionary<string, string[]> CalculateBundleDependencies(List<List<string>> assetFileList, Dictionary<string, string> filenameToBundleName)
// Inputs: assetFileList = per asset list of unique file dependencies, first entry is the main dependency
// filenameToBundleName = mapping of file name to asset bundle name
// Output: mapping of bundle name to array of bundle name dependencies
List<List<string>> assetFileList = new List<List<string>>();
assetFileList.Add(new List<string> { "file1", "file2" });
assetFileList.Add(new List<string> { "file2", "file3" });
Dictionary<string, string> filenameToBundleName = new Dictionary<string, string>();
filenameToBundleName.Add("file1", "bundle1");
filenameToBundleName.Add("file2", "bundle2");
filenameToBundleName.Add("file3", "bundle3");
Dictionary<string, string[]> results = ArchiveAndCompressBundles.CalculateBundleDependencies(assetFileList, filenameToBundleName);
CollectionAssert.AreEquivalent(new string[] { "bundle1", "bundle2", "bundle3" }, results.Keys);
CollectionAssert.AreEquivalent(new string[] { "bundle2", "bundle3" }, results["bundle1"]);
CollectionAssert.AreEquivalent(new string[] { "bundle3" }, results["bundle2"]);
CollectionAssert.AreEquivalent(new string[] { }, results["bundle3"]);
}
[Test]
public void CalculateBundleDependencies_ReturnsRecursiveDependencies_ForRecursiveInputs()
{
// Dictionary<string, string[]> CalculateBundleDependencies(List<List<string>> assetFileList, Dictionary<string, string> filenameToBundleName)
// Inputs: assetFileList = per asset list of unique file dependencies, first entry is the main dependency
// filenameToBundleName = mapping of file name to asset bundle name
// Output: mapping of bundle name to array of bundle name dependencies
List<List<string>> assetFileList = new List<List<string>>();
assetFileList.Add(new List<string> { "file1", "file2", "file3" });
assetFileList.Add(new List<string> { "file2", "file3" });
Dictionary<string, string> filenameToBundleName = new Dictionary<string, string>();
filenameToBundleName.Add("file1", "bundle1");
filenameToBundleName.Add("file2", "bundle2");
filenameToBundleName.Add("file3", "bundle3");
Dictionary<string, string[]> results = ArchiveAndCompressBundles.CalculateBundleDependencies(assetFileList, filenameToBundleName);
CollectionAssert.AreEquivalent(new string[] { "bundle1", "bundle2", "bundle3" }, results.Keys);
CollectionAssert.AreEquivalent(new string[] { "bundle2", "bundle3" }, results["bundle1"]);
CollectionAssert.AreEquivalent(new string[] { "bundle3" }, results["bundle2"]);
CollectionAssert.AreEquivalent(new string[] { }, results["bundle3"]);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c2c7d61e655a9134eb902da0ddc663ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.ScriptableBuildPipeline.Editor.Tests")]
[assembly: InternalsVisibleTo("PerformanceTests.Editor")]

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0437b76f03e61424abbe630113ecd0f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,249 @@
using NUnit.Framework;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
class AssetLoadInfoSortingTests
{
const string kTestAssetFolder = "Assets/TestAssets";
const string kTestAsset = "Assets/TestAssets/SpriteTexture32x32.png";
[OneTimeSetUp]
public void OneTimeSetUp()
{
Directory.CreateDirectory(kTestAssetFolder);
CreateTestSpriteTexture();
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
Directory.Delete(kTestAssetFolder, true);
File.Delete(kTestAssetFolder + ".meta");
AssetDatabase.Refresh();
}
static void CreateTestSpriteTexture()
{
var data = ImageConversion.EncodeToPNG(new Texture2D(32, 32));
File.WriteAllBytes(kTestAsset, data);
AssetDatabase.ImportAsset(kTestAsset, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate);
var importer = AssetImporter.GetAtPath(kTestAsset) as TextureImporter;
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Multiple;
importer.spritesheet = new[]
{
new SpriteMetaData
{
name = "WhiteTexture32x32_0",
rect = new Rect(0, 19, 32, 13),
alignment = 0,
pivot = new Vector2(0.5f, 0.5f),
border = new Vector4(0, 0, 0, 0)
},
new SpriteMetaData
{
name = "WhiteTexture32x32_1",
rect = new Rect(4, 19, 24, 11),
alignment = 0,
pivot = new Vector2(0.5f, 0.5f),
border = new Vector4(0, 0, 0, 0)
},
new SpriteMetaData
{
name = "WhiteTexture32x32_2",
rect = new Rect(9, 5, 12, 7),
alignment = 0,
pivot = new Vector2(0.5f, 0.5f),
border = new Vector4(0, 0, 0, 0)
}
};
importer.SaveAndReimport();
}
static AssetLoadInfo GetTestAssetLoadInfo()
{
GUID asset = new GUID(AssetDatabase.AssetPathToGUID(kTestAsset));
ObjectIdentifier[] oId = ContentBuildInterface.GetPlayerObjectIdentifiersInAsset(asset, EditorUserBuildSettings.activeBuildTarget);
AssetLoadInfo loadInfo = new AssetLoadInfo()
{
asset = asset,
address = kTestAsset,
includedObjects = oId.ToList(),
referencedObjects = new List<ObjectIdentifier>()
};
return loadInfo;
}
static List<AssetLoadInfo> GetTestAssetLoadInfoList()
{
List<AssetLoadInfo> testList = new List<AssetLoadInfo>
{
GetTestAssetLoadInfo(),
GetTestAssetLoadInfo()
};
return testList;
}
[Test]
public void AssetLoadInfo_SortsByGuid()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
testList[1].asset = new GUID();
var asset0 = testList[0].asset;
var asset1 = testList[1].asset;
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.AreEqual(asset1, testList[0].asset);
Assert.AreEqual(asset0, testList[1].asset);
}
[Test]
public void AssetLoadInfo_SortsByGuid_PreSorted()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
testList[0].asset = new GUID();
var asset0 = testList[0].asset;
var asset1 = testList[1].asset;
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.AreEqual(asset0, testList[0].asset);
Assert.AreEqual(asset1, testList[1].asset);
}
[Test]
public void AssetLoadInfo_SortsByIncludedObjects()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
// Verify which one we should swap to ensure current ordering is different from expected ordering.
if (testList[0].includedObjects[0].localIdentifierInFile < testList[0].includedObjects[1].localIdentifierInFile)
testList[0].includedObjects.Swap(0, 1);
else
testList[1].includedObjects.Swap(0, 1);
var object0 = testList[0].includedObjects[0];
var object1 = testList[1].includedObjects[0];
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.AreEqual(object1, testList[0].includedObjects[0]);
Assert.AreEqual(object0, testList[1].includedObjects[0]);
}
[Test]
public void AssetLoadInfo_SortsByIncludedObjects_PreSorted()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
// Verify which one we should swap to ensure current ordering is different from expected ordering.
if (testList[0].includedObjects[0].localIdentifierInFile < testList[0].includedObjects[1].localIdentifierInFile)
testList[1].includedObjects.Swap(0, 1);
else
testList[0].includedObjects.Swap(0, 1);
var object0 = testList[0].includedObjects[0];
var object1 = testList[1].includedObjects[0];
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.AreEqual(object0, testList[0].includedObjects[0]);
Assert.AreEqual(object1, testList[1].includedObjects[0]);
}
[Test]
public void AssetLoadInfo_SortsByIncludedObjects_SortsNullIncludedObjects()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
testList[1].includedObjects = null;
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.IsNull(testList[0].includedObjects);
Assert.IsNotNull(testList[1].includedObjects);
}
[Test]
public void AssetLoadInfo_SortsByIncludedObjects_SortsNullIncludedObjects_PreSorted()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
testList[0].includedObjects = null;
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.IsNull(testList[0].includedObjects);
Assert.IsNotNull(testList[1].includedObjects);
}
[Test]
public void AssetLoadInfo_SortsByAddress()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
testList[1].address = "A/short/path";
var address0 = testList[0].address;
var address1 = testList[1].address;
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.AreEqual(address1, testList[0].address);
Assert.AreEqual(address0, testList[1].address);
}
[Test]
public void AssetLoadInfo_SortsByAddress_PreSorted()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
testList[0].address = "A/short/path";
var address0 = testList[0].address;
var address1 = testList[1].address;
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.AreEqual(address0, testList[0].address);
Assert.AreEqual(address1, testList[1].address);
}
[Test]
public void AssetLoadInfo_SortsByAddress_SortsNullAdress()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
testList[1].address = null;
var address0 = testList[0].address;
var address1 = testList[1].address;
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.AreEqual(address1, testList[0].address);
Assert.AreEqual(address0, testList[1].address);
}
[Test]
public void AssetLoadInfo_SortsByAddress_SortsNullAdress_PreSorted()
{
List<AssetLoadInfo> testList = GetTestAssetLoadInfoList();
testList[0].address = null;
var address0 = testList[0].address;
var address1 = testList[1].address;
testList.Sort(GenerateBundleCommands.AssetLoadInfoCompare);
Assert.AreEqual(address0, testList[0].address);
Assert.AreEqual(address1, testList[1].address);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8294b087f18cdb49a6dc0c48dccb3a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,459 @@
using System;
using NUnit.Framework;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
abstract internal class BuildCacheTestBase
{
protected const string kBuildCacheTestPath = "Assets/BuildCacheTestAssets";
protected string kTestFile1
{
get { return Path.Combine(kBuildCacheTestPath, "testfile1.txt"); }
}
protected string kUncachedTestFilename
{
get { return Path.Combine(kBuildCacheTestPath, "uncached.txt"); }
}
protected string kTempAssetFilename
{
get { return Path.Combine(kBuildCacheTestPath, "temporary.txt"); }
}
protected string kTestScenePath
{
get { return Path.Combine(kBuildCacheTestPath, "testScene.unity"); }
}
protected GUID TestFile1GUID
{
get { return new GUID(AssetDatabase.AssetPathToGUID(kTestFile1)); }
}
protected GUID UncachedGUID
{
get { return new GUID(AssetDatabase.AssetPathToGUID(kUncachedTestFilename)); }
}
protected GUID TempAssetGUID
{
get { return new GUID(AssetDatabase.AssetPathToGUID(kTempAssetFilename)); }
}
protected GUID TestSceneGUID
{
get { return new GUID(AssetDatabase.AssetPathToGUID(kTestScenePath)); }
}
protected BuildCache m_Cache;
internal virtual void OneTimeSetupDerived() {}
internal virtual void OneTimeTearDownDerived() {}
internal virtual void SetupDerived() {}
internal virtual void TeardownDerived() {}
[OneTimeSetUp]
public void OneTimeSetUp()
{
Directory.CreateDirectory(kBuildCacheTestPath);
File.WriteAllText(kTestFile1, "t1");
File.WriteAllText(kUncachedTestFilename, "uncached");
File.WriteAllText(kTempAssetFilename, "delete me");
AssetDatabase.Refresh();
OneTimeSetupDerived();
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
Directory.Delete(kBuildCacheTestPath, true);
File.Delete(kBuildCacheTestPath + ".meta");
AssetDatabase.Refresh();
OneTimeTearDownDerived();
}
[SetUp]
public void Setup()
{
BuildCacheUtility.ClearCacheHashes();
PurgeBuildCache();
RecreateBuildCache();
SetupDerived();
}
[TearDown]
public void TearDown()
{
if (m_Cache != null)
{
m_Cache.Dispose();
m_Cache = null;
}
TeardownDerived();
}
protected virtual void RecreateBuildCache()
{
BuildCacheUtility.ClearCacheHashes();
if (m_Cache != null)
{
m_Cache.SyncPendingSaves();
m_Cache.Dispose();
m_Cache = null;
}
m_Cache = new BuildCache();
}
protected virtual void PurgeBuildCache()
{
if (m_Cache != null)
m_Cache.SyncPendingSaves();
BuildCache.PurgeCache(false);
}
static protected CachedInfo LoadCachedInfoForGUID(BuildCache cache, GUID guid)
{
IList<CachedInfo> infos;
CacheEntry entry1 = cache.GetCacheEntry(guid);
cache.LoadCachedData(new List<CacheEntry>() { entry1 }, out infos);
return infos[0];
}
static protected string StoreDataInCacheWithGUID(BuildCache cache, GUID guid, object data, GUID depGUID = new GUID())
{
List<CacheEntry> deps = new List<CacheEntry>();
if (!depGUID.Empty())
deps.Add(cache.GetCacheEntry(depGUID));
CacheEntry entry1 = cache.GetCacheEntry(guid);
CachedInfo info = new CachedInfo();
info.Asset = entry1;
info.Dependencies = deps.ToArray();
info.Data = new object[] { data };
cache.SaveCachedData(new List<CachedInfo>() { info });
cache.SyncPendingSaves();
return cache.GetCachedInfoFile(info.Asset);
}
static protected GUID CreateTestTextAsset(string contents)
{
string filename;
return CreateTestTextAsset(contents, out filename);
}
static protected GUID CreateTestTextAsset(string contents, out string filename)
{
int fileIndex = 0;
while (true)
{
filename = Path.Combine(kBuildCacheTestPath, "testasset" + fileIndex);
if (!File.Exists(filename))
{
File.WriteAllText(filename, contents);
AssetDatabase.Refresh();
return new GUID(AssetDatabase.AssetPathToGUID(filename));
}
fileIndex++;
}
}
static protected void ModifyTestTextAsset(GUID guid, string text)
{
string filename = AssetDatabase.GUIDToAssetPath(guid.ToString());
File.WriteAllText(filename, text);
AssetDatabase.ImportAsset(filename);
}
[Test]
public void WhenLoadingCachedDataForGUIDWithModifiedDependency_CachedInfoIsNull()
{
GUID depGuid = CreateTestTextAsset("mytext");
StoreDataInCacheWithGUID(m_Cache, TestFile1GUID, "data", depGuid);
ModifyTestTextAsset(depGuid, "mytext2");
RecreateBuildCache();
CachedInfo info = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);
Assert.IsNull(info);
}
[Test]
public void WhenLoadingCachedDataForModifiedGUID_CachedInfoIsNull()
{
GUID guid = CreateTestTextAsset("mytext");
StoreDataInCacheWithGUID(m_Cache, guid, "data");
ModifyTestTextAsset(guid, "mytext2");
RecreateBuildCache();
CachedInfo info = LoadCachedInfoForGUID(m_Cache, guid);
Assert.IsNull(info);
}
[Test]
public void WhenLoadingCachedDataForGUIDWithInvalidCacheData_CachedInfoIsNull()
{
GUID depGuid = CreateTestTextAsset("mytext");
string path = StoreDataInCacheWithGUID(m_Cache, TestFile1GUID, "data", depGuid);
RecreateBuildCache();
File.WriteAllText(path, "Invalidating cache file! Good luck deserializing this! =P");
CachedInfo info = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);
Assert.IsNull(info);
}
[Test]
public void WhenLoadingStoredCachedData_CachedInfoIsValid()
{
StoreDataInCacheWithGUID(m_Cache, TestFile1GUID, "data");
RecreateBuildCache();
CachedInfo info = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);
Assert.AreEqual("data", (string)info.Data[0]);
}
[Test]
public void WhenLoadingUncachedData_CachedInfoIsNull()
{
CachedInfo info = LoadCachedInfoForGUID(m_Cache, UncachedGUID);
Assert.IsNull(info);
}
[Test]
public void WhenGlobalVersionChanges_OriginalCachedInfoDoesNotNeedRebuild()
{
m_Cache.OverrideGlobalHash(new Hash128(0, 1, 0, 0));
StoreDataInCacheWithGUID(m_Cache, TestFile1GUID, "data");
CachedInfo info1 = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);
RecreateBuildCache();
m_Cache.OverrideGlobalHash(new Hash128(0, 2, 0, 0));
CachedInfo info2 = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);
Assert.IsFalse(m_Cache.HasAssetOrDependencyChanged(info1)); // original info still doesn't need rebuild. Required for content update
Assert.IsNull(info2); // however the data cannot be recovered
}
[Test]
public void WhenLocalVersionChanges_AssetReturnsDifferentCacheEntry()
{
GUID guid = CreateTestTextAsset("mytext");
var entry1 = m_Cache.GetCacheEntry(guid, 2);
var entry2 = m_Cache.GetCacheEntry(guid, 4);
Assert.AreEqual(entry1.Guid, entry2.Guid);
Assert.AreEqual(entry1.File, entry2.File);
Assert.AreEqual(entry1.Type, entry2.Type);
Assert.AreNotEqual(entry1.Version, entry2.Version);
Assert.AreNotEqual(entry1.Hash, entry2.Hash);
}
[Test]
public void WhenLocalVersionChanges_FileReturnsDifferentCacheEntry()
{
string filename;
CreateTestTextAsset("mytext", out filename);
var entry1 = m_Cache.GetCacheEntry(filename, 2);
var entry2 = m_Cache.GetCacheEntry(filename, 4);
Assert.AreEqual(entry1.Guid, entry2.Guid);
Assert.AreEqual(entry1.File, entry2.File);
Assert.AreEqual(entry1.Type, entry2.Type);
Assert.AreNotEqual(entry1.Version, entry2.Version);
Assert.AreNotEqual(entry1.Hash, entry2.Hash);
}
[Test]
public void GetUpdatedCacheEntry_ReturnsCacheEntryWithSameVersionAndHash_IfAssetHasNotChanged()
{
GUID guid = CreateTestTextAsset("mytext");
var entry1 = m_Cache.GetCacheEntry(guid, 2);
m_Cache.ClearCacheEntryMaps();
var entry2 = m_Cache.GetUpdatedCacheEntry(entry1);
Assert.AreEqual(entry1.Guid, entry2.Guid);
Assert.AreEqual(entry1.File, entry2.File);
Assert.AreEqual(entry1.Type, entry2.Type);
Assert.AreEqual(entry1.Version, entry2.Version);
Assert.AreEqual(entry1.Hash, entry2.Hash);
}
[Test]
public void GetUpdatedCacheEntry_ReturnsCacheEntryWithSameVersionAndHash_IfFileHasNotChanged()
{
string filename;
CreateTestTextAsset("mytext", out filename);
var entry1 = m_Cache.GetCacheEntry(filename, 2);
m_Cache.ClearCacheEntryMaps();
var entry2 = m_Cache.GetUpdatedCacheEntry(entry1);
Assert.AreEqual(entry1.Guid, entry2.Guid);
Assert.AreEqual(entry1.File, entry2.File);
Assert.AreEqual(entry1.Type, entry2.Type);
Assert.AreEqual(entry1.Version, entry2.Version);
Assert.AreEqual(entry1.Hash, entry2.Hash);
}
[Test]
public void GetCacheEntry_InvalidPath_ReturnsInvalidCacheEntry()
{
var entry = m_Cache.GetCacheEntry("this/path/does/not/exist");
Assert.IsFalse(entry.IsValid());
}
[Test]
public void GetCacheEntry_InvalidGUID_ReturnsInvalidCacheEntry()
{
var entry = m_Cache.GetCacheEntry(new GUID("00000000000000000000000000000000"));
Assert.IsFalse(entry.IsValid());
}
[Test]
public void GetCacheEntry_FormerValidPathAndGUID_ReturnsInvalidCacheEntry()
{
var tempAssetGUID = TempAssetGUID;
var entry1 = m_Cache.GetCacheEntry(kTempAssetFilename);
var entry2 = m_Cache.GetCacheEntry(tempAssetGUID);
Assert.IsTrue(entry1.IsValid());
Assert.IsTrue(entry2.IsValid());
File.Delete(kTempAssetFilename);
AssetDatabase.Refresh();
m_Cache.ClearCacheEntryMaps();
entry1 = m_Cache.GetCacheEntry(kTempAssetFilename);
entry2 = m_Cache.GetCacheEntry(tempAssetGUID);
Assert.IsFalse(entry1.IsValid());
Assert.IsFalse(entry2.IsValid());
}
[Test]
public void GetCacheEntry_PathForAsset_ReturnsAssetBasedCacheEntry()
{
var entry1 = m_Cache.GetCacheEntry(kTestFile1);
var entry2 = m_Cache.GetCacheEntry(TestFile1GUID);
Assert.IsTrue(entry1.Type == CacheEntry.EntryType.Asset);
Assert.AreEqual(entry2, entry1);
}
[Test]
public void GetCacheEntry_DiffStripUnusedMeshComponentsSettings_ReturnsDiffHashes()
{
Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
EditorSceneManager.SaveScene(scene, kTestScenePath);
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); // clear active scene
int version = 2;
var kvp = new KeyValuePair<GUID, int>(TestSceneGUID, version);
bool stripUnusedMeshComponents = PlayerSettings.stripUnusedMeshComponents;
try
{
PlayerSettings.stripUnusedMeshComponents = false;
CacheEntry entry1 = m_Cache.GetCacheEntry(TestSceneGUID, version);
BuildCacheUtility.m_GuidToHash.Remove(kvp);
PlayerSettings.stripUnusedMeshComponents = true;
CacheEntry entry2 = m_Cache.GetCacheEntry(TestSceneGUID);
BuildCacheUtility.m_GuidToHash.Remove(kvp);
Assert.AreNotEqual(entry1.Hash, entry2.Hash);
}
finally
{
PlayerSettings.stripUnusedMeshComponents = stripUnusedMeshComponents;
AssetDatabase.DeleteAsset(kTestScenePath);
}
}
[Test]
public void GetCachedInfoFile_IsInside_GetCachedArtifactsDirectory()
{
var entry = m_Cache.GetCacheEntry(kTestFile1);
var infoFile = m_Cache.GetCachedInfoFile(entry);
var artifactsDirectory = m_Cache.GetCachedArtifactsDirectory(entry);
StringAssert.Contains(artifactsDirectory, infoFile);
}
void WriteRandomFile(string fileName, int sizeInMB)
{
byte[] data = new byte[sizeInMB * 1024 * 1024];
var rng = new System.Random();
rng.NextBytes(data);
File.WriteAllBytes(fileName, data);
}
void PopulateCache(out int filesWritten, out string[] artifactsDirectories)
{
var entries = new[]
{
m_Cache.GetCacheEntry(kTestFile1),
m_Cache.GetCacheEntry(kUncachedTestFilename)
};
artifactsDirectories = new[]
{
Path.GetFullPath(m_Cache.GetCachedArtifactsDirectory(entries[0])),
Path.GetFullPath(m_Cache.GetCachedArtifactsDirectory(entries[1]))
};
// Setup cache
filesWritten = 0;
foreach (var directory in artifactsDirectories)
{
Directory.CreateDirectory(directory);
for (int i = 0; i < 2; i++)
{
WriteRandomFile($"{directory}/cachefile_{i}.bytes", 2);
filesWritten++;
}
}
}
[Test]
public void PruneCache_ComputeCacheSizeAndFolders_ReturnsCorrectSizeAndFolders()
{
PopulateCache(out int filesWritten, out string[] artifactsDirectories);
BuildCache.ComputeCacheSizeAndFolders(out long currentCacheSize, out List<BuildCache.CacheFolder> cacheFolders);
//filesWritten * 2mb each * 1024 to kb * 1024 to b
Assert.AreEqual(filesWritten * 2 * 1024 * 1024, currentCacheSize);
Assert.AreEqual(artifactsDirectories.Length, cacheFolders.Count);
var folders = cacheFolders.Select(x => x.directory.FullName);
CollectionAssert.AreEquivalent(artifactsDirectories, folders);
}
[Test]
public void PruneCache_PruneCacheFolders_WillRemoveOldestFolders()
{
PopulateCache(out int filesWritten, out string[] artifactsDirectories);
BuildCache.ComputeCacheSizeAndFolders(out long currentCacheSize, out List<BuildCache.CacheFolder> cacheFolders);
// Set folder older
var folder = cacheFolders[0];
folder.LastAccessTimeUtc = folder.LastAccessTimeUtc.Subtract(new TimeSpan(1, 0, 0));
cacheFolders[0] = folder;
// delete just under the first folder size
long maximumCacheSize = currentCacheSize - folder.Length + 1;
BuildCache.PruneCacheFolders(maximumCacheSize, currentCacheSize, cacheFolders);
BuildCache.ComputeCacheSizeAndFolders(out long newCurrentCacheSize, out List<BuildCache.CacheFolder> newCacheFolders);
Assert.AreNotEqual(0, newCurrentCacheSize);
Assert.GreaterOrEqual(maximumCacheSize, newCurrentCacheSize);
Assert.AreEqual(artifactsDirectories.Length - 1, newCacheFolders.Count);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cd396e02e0371554e9bfc4ebb8035318
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,48 @@
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
class MyContextObjectClass : IContextObject
{
}
interface ITestInterfaceWithContextDerivation : IContextObject {}
class TestITestInterfaceWithContextDerivationImplementation : ITestInterfaceWithContextDerivation
{}
public class BuildContextTests
{
BuildContext m_Ctx;
[SetUp]
public void Setup()
{
m_Ctx = new BuildContext();
}
[Test]
public void SetContextObject_WhenTypeDoesNotExist_AddsContextObject()
{
m_Ctx.SetContextObject(new MyContextObjectClass());
Assert.NotNull(m_Ctx.GetContextObject<MyContextObjectClass>());
}
[Test]
public void SetContextObject_WhenTypeHasInterfaceAssignableToContextObject_InterfaceAndObjectTypeUsedAsKey()
{
m_Ctx.SetContextObject(new TestITestInterfaceWithContextDerivationImplementation());
Assert.NotNull(m_Ctx.GetContextObject<ITestInterfaceWithContextDerivation>());
Assert.NotNull(m_Ctx.GetContextObject<TestITestInterfaceWithContextDerivationImplementation>());
}
[Test]
public void GetContextObject_WhenTypeDoesNotExist_Throws()
{
Assert.Throws(typeof(Exception), () => m_Ctx.GetContextObject<ITestInterfaceWithContextDerivation>());
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2406ad24700d76e4da5005529a6bafce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,158 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Utilities;
using static UnityEditor.Build.Pipeline.Utilities.BuildLog;
namespace UnityEditor.Build.Pipeline.Tests
{
public class BuildLogTests
{
[Test]
public void WhenBeginAndEndScope_DurationIsCorrect()
{
BuildLog log = new BuildLog();
using (log.ScopedStep(LogLevel.Info, "TestStep"))
Thread.Sleep(5);
Assert.AreEqual("TestStep", log.Root.Children[0].Name);
Assert.Greater(log.Root.Children[0].DurationMS, 4);
}
[Test]
public void WhenAddMessage_EntryIsCreated()
{
BuildLog log = new BuildLog();
using (log.ScopedStep(LogLevel.Info, "TestStep"))
log.AddEntry(LogLevel.Info, "TestEntry");
Assert.AreEqual("TestEntry", log.Root.Children[0].Entries[0].Message);
}
[Test]
public void WhenMessageAddedWithScope_EntryIsCreated()
{
BuildLog log = new BuildLog();
((IDisposable)log.ScopedStep(LogLevel.Info, "TestStep", "TestEntry")).Dispose();
Assert.AreEqual("TestEntry", log.Root.Children[0].Entries[0].Message);
}
[Test]
public void WhenScopeIsThreaded_AndThreadAddsNode_NodeEnteredInThreadedScope()
{
BuildLog log = new BuildLog();
using (log.ScopedStep(LogLevel.Info, "TestStep", true))
{
var t = new Thread(() =>
{
log.AddEntry(LogLevel.Info, "ThreadedMsg1");
using (log.ScopedStep(LogLevel.Info, "ThreadedStep"))
{
log.AddEntry(LogLevel.Info, "ThreadedMsg2");
}
});
t.Start();
t.Join();
}
Assert.AreEqual("ThreadedMsg1", log.Root.Children[0].Entries[0].Message);
Assert.AreNotEqual(Thread.CurrentThread.ManagedThreadId, log.Root.Children[0].Entries[0].ThreadId);
Assert.AreEqual("ThreadedStep", log.Root.Children[0].Children[0].Name);
Assert.AreNotEqual(Thread.CurrentThread.ManagedThreadId, log.Root.Children[0].Children[0].ThreadId);
Assert.AreEqual("ThreadedMsg2", log.Root.Children[0].Children[0].Entries[0].Message);
Assert.AreNotEqual(Thread.CurrentThread.ManagedThreadId, log.Root.Children[0].Children[0].Entries[0].ThreadId);
}
[Test]
public void WhenBeginAndEndScopeOnThread_StartAndEndTimeAreWithinMainThreadScope()
{
BuildLog log = new BuildLog();
using (log.ScopedStep(LogLevel.Info, "TestStep", true))
{
var t = new Thread(() =>
{
Thread.Sleep(1);
log.AddEntry(LogLevel.Info, "ThreadedMsg1");
Thread.Sleep(1);
using (log.ScopedStep(LogLevel.Info, "ThreadedStep"))
{
Thread.Sleep(2);
using (log.ScopedStep(LogLevel.Info, "ThreadedStepNested"))
Thread.Sleep(2);
}
Thread.Sleep(1);
});
t.Start();
t.Join();
}
double testStepStart = log.Root.Children[0].StartTime;
double threadedMessageStart = log.Root.Children[0].Entries[0].Time;
double threadedScopeStart = log.Root.Children[0].Children[0].StartTime;
double threadedScopeEnd = threadedScopeStart + log.Root.Children[0].Children[0].DurationMS;
double threadedScopeNestedStart = log.Root.Children[0].Children[0].Children[0].StartTime;
double testStepEnd = testStepStart + log.Root.Children[0].DurationMS;
Assert.Less(threadedScopeStart, threadedScopeNestedStart);
Assert.Less(testStepStart, threadedMessageStart);
Assert.Less(threadedMessageStart, threadedScopeStart);
Assert.Less(threadedScopeStart, threadedScopeEnd);
Assert.Less(threadedScopeEnd, testStepEnd);
}
[Test]
public void WhenConvertingToTraceEventFormat_BackslashesAreEscaped()
{
BuildLog log = new BuildLog();
using (log.ScopedStep(LogLevel.Info, "TestStep\\AfterSlash"))
log.AddEntry(LogLevel.Info, "TestEntry\\AfterSlash");
string text = log.FormatForTraceEventProfiler();
StringAssert.Contains("TestStep\\\\AfterSlash", text);
StringAssert.Contains("TestEntry\\\\AfterSlash", text);
}
[Test]
public void WhenConvertingToTraceEventFormat_MetaDataIsAdded()
{
BuildLog log = new BuildLog();
log.AddMetaData("SOMEKEY", "SOMEVALUE");
string text = log.FormatForTraceEventProfiler();
StringAssert.Contains("SOMEKEY", text);
StringAssert.Contains("SOMEVALUE", text);
}
#if UNITY_2020_2_OR_NEWER || ENABLE_DETAILED_PROFILE_CAPTURING
[Test]
public void WhenBeginAndEndDeferredEventsDontMatchUp_HandleDeferredEventsStream_ThrowsException()
{
BuildLog log = new BuildLog();
DeferredEvent startEvent = new DeferredEvent() { Type = DeferredEventType.Begin };
List<DeferredEvent> events = new List<DeferredEvent>() { startEvent };
Assert.Throws<Exception>(() => log.HandleDeferredEventStreamInternal(events));
}
[Test]
public void WhenBeginAndEndDeferredEventsMatchUp_HandleDeferredEventsStream_CreatesLogEvents()
{
BuildLog log = new BuildLog();
DeferredEvent startEvent = new DeferredEvent() { Name = "Start", Type = DeferredEventType.Begin };
DeferredEvent endEvent = new DeferredEvent() { Name = "End", Type = DeferredEventType.End };
List<DeferredEvent> events = new List<DeferredEvent>() { startEvent, endEvent };
log.HandleDeferredEventStreamInternal(events);
Assert.AreEqual(startEvent.Name, log.Root.Children[0].Name);
}
[Test]
public void WhenDeferredEventsAreOnlyInfoTypes_HandleDeferredEventsStream_CreatesLogEntry()
{
BuildLog log = new BuildLog();
DeferredEvent infoEvent = new DeferredEvent() { Name = "Info", Type = DeferredEventType.Info };
List<DeferredEvent> events = new List<DeferredEvent>() { infoEvent };
log.HandleDeferredEventStreamInternal(events);
Assert.AreEqual(infoEvent.Name, log.Root.Entries[0].Message);
}
#endif
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9b3cd90055a04b48b05f0e458e05c07
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c655781b9e2c70945bf8e3f232c54d6e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,91 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
namespace UnityEditor.CacheServerTests
{
internal class ByteArrayStream : Stream
{
private int m_pos;
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return true; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Length
{
get { return BackingBuffer.Length; }
}
public override long Position
{
get { return m_pos; }
set
{
m_pos = Math.Min((int)value, BackingBuffer.Length - 1);
Debug.Assert(m_pos >= 0);
}
}
public byte[] BackingBuffer { get; private set; }
public ByteArrayStream(long size)
{
BackingBuffer = new byte[size];
RandomNumberGenerator.Create().GetBytes(BackingBuffer);
}
public override void SetLength(long value) {}
public override void Flush() {}
public override void Write(byte[] buffer, int offset, int count)
{
Debug.Assert(count <= BackingBuffer.Length - m_pos); // can't write past out buffer length
count = Math.Min(count, buffer.Length - offset); // Don't read past the input buffer length
Buffer.BlockCopy(buffer, offset, BackingBuffer, m_pos, count);
m_pos += count;
}
public override int Read(byte[] buffer, int offset, int count)
{
count = Math.Min(count, BackingBuffer.Length - m_pos); // Don't copy more bytes than we have
count = Math.Min(count, buffer.Length - offset); // Don't overrun the destination buffer
Buffer.BlockCopy(BackingBuffer, m_pos, buffer, offset, count);
m_pos += count;
return count;
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
Position = (int)offset;
break;
case SeekOrigin.Current:
Position += (int)offset;
break;
case SeekOrigin.End:
Position = BackingBuffer.Length - (int)offset - 1;
break;
default:
throw new ArgumentOutOfRangeException("origin", origin, null);
}
return Position;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8cce051c0b60040d68cd27a80e58f787
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,133 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using UnityEditor;
using UnityEngine;
namespace UnityEditor.CacheServerTests
{
internal class LocalCacheServer : ScriptableSingleton<LocalCacheServer>
{
[SerializeField] public string m_path;
[SerializeField] public int m_port;
[SerializeField] public int m_pid = -1;
private void Create(int port, ulong size, string cachePath)
{
var nodeExecutable = Utils.Paths.Combine(EditorApplication.applicationContentsPath, "Tools", "nodejs");
nodeExecutable = Application.platform == RuntimePlatform.WindowsEditor
? Utils.Paths.Combine(nodeExecutable, "node.exe")
: Utils.Paths.Combine(nodeExecutable, "bin", "node");
if (!Directory.Exists(cachePath))
Directory.CreateDirectory(cachePath);
m_path = cachePath;
var cacheServerJs = Utils.Paths.Combine(EditorApplication.applicationContentsPath, "Tools", "CacheServer", "main.js");
var processStartInfo = new ProcessStartInfo(nodeExecutable)
{
Arguments = "\"" + cacheServerJs + "\""
+ " --port " + port
+ " --path \"" + m_path
+ "\" --nolegacy"
+ " --monitor-parent-process " + Process.GetCurrentProcess().Id
// node.js has issues running on windows with stdout not redirected.
// so we silence logging to avoid that. And also to avoid CacheServer
// spamming the editor logs on OS X.
+ " --silent"
+ " --size " + size,
UseShellExecute = false,
CreateNoWindow = true
};
var p = new Process { StartInfo = processStartInfo };
p.Start();
m_port = port;
m_pid = p.Id;
Save(true);
}
public static string CachePath
{
get { return instance.m_path; }
}
public static int Port
{
get { return instance.m_port; }
}
public static void Setup(ulong size, string cachePath)
{
Kill();
instance.Create(GetRandomUnusedPort(), size, cachePath);
WaitForServerToComeAlive(instance.m_port);
}
public static void Kill()
{
if (instance.m_pid == -1)
return;
try
{
var p = Process.GetProcessById(instance.m_pid);
p.Kill();
instance.m_pid = -1;
}
catch
{
// if we could not get a process, there is non alive. continue.
}
}
public static void Clear()
{
Kill();
if (Directory.Exists(instance.m_path))
Directory.Delete(instance.m_path, true);
}
private static void WaitForServerToComeAlive(int port)
{
var start = DateTime.Now;
var maximum = start.AddSeconds(5);
while (DateTime.Now < maximum)
{
if (!PingHost("localhost", port, 10)) continue;
Console.WriteLine("Server Came alive after {0} ms", (DateTime.Now - start).TotalMilliseconds);
break;
}
}
private static int GetRandomUnusedPort()
{
var listener = new TcpListener(IPAddress.Any, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static bool PingHost(string host, int port, int timeout)
{
try
{
using (var client = new TcpClient())
{
var result = client.BeginConnect(host, port, null, null);
result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(timeout));
return client.Connected;
}
}
catch
{
return false;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1b623084ee8eb4b9199e34858add3cac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,30 @@
using System.IO;
using UnityEditor.Build.CacheServer;
namespace UnityEditor.CacheServerTests
{
internal class TestDownloadItem : IDownloadItem
{
private ByteArrayStream m_writeStream;
public FileId Id { get; private set; }
public FileType Type { get; private set; }
public void Finish() {}
public byte[] Bytes
{
get { return m_writeStream.BackingBuffer; }
}
public Stream GetWriteStream(long size)
{
return m_writeStream ?? (m_writeStream = new ByteArrayStream(size));
}
public TestDownloadItem(FileId fileId, FileType fileType)
{
Id = fileId;
Type = fileType;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b734b613791eb4b0381013f84745a995
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,460 @@
using System;
using System.IO;
using System.Threading;
using NUnit.Framework;
using UnityEditor.Build.CacheServer;
using UnityEngine;
using Random = System.Random;
namespace UnityEditor.CacheServerTests
{
[TestFixture]
public class Tests
{
private const string KTestHost = "127.0.0.1";
private const string KInvalidTestHost = "192.0.2.1";
private Random rand;
private static int TestPort
{
get { return LocalCacheServer.Port; }
}
private FileId GenerateFileId()
{
if (rand == null)
rand = new Random();
var guid = new byte[16];
var hash = new byte[16];
rand.NextBytes(guid);
rand.NextBytes(hash);
return FileId.From(guid, hash);
}
[OneTimeSetUp]
public void BeforeAll()
{
var cachePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
LocalCacheServer.Setup(1024 * 1024, cachePath);
rand = new Random();
}
[OneTimeTearDown]
public void AfterAll()
{
LocalCacheServer.Clear();
}
[Test]
public void FileDownloadItem()
{
var fileId = GenerateFileId();
var readStream = new ByteArrayStream(128 * 1024);
var client = new Client(KTestHost, TestPort);
client.Connect();
client.BeginTransaction(fileId);
client.Upload(FileType.Asset, readStream);
client.EndTransaction();
Thread.Sleep(50); // give the server a little time to finish the transaction
var targetFile = Path.GetTempFileName();
var downloadItem = new FileDownloadItem(fileId, FileType.Asset, targetFile);
var mre = new ManualResetEvent(false);
Exception err = null;
client.DownloadFinished += (sender, args) =>
{
try
{
Assert.AreEqual(DownloadResult.Success, args.Result);
Assert.AreEqual(args.DownloadItem.Id, fileId);
Assert.IsTrue(File.Exists(targetFile));
var fileBytes = File.ReadAllBytes(targetFile);
Assert.IsTrue(Util.ByteArraysAreEqual(readStream.BackingBuffer, fileBytes));
}
catch (Exception e)
{
err = e;
}
finally
{
if (File.Exists(targetFile))
File.Delete(targetFile);
mre.Set();
}
};
client.QueueDownload(downloadItem);
Assert.IsTrue(mre.WaitOne(2000));
if (err != null)
throw err;
}
[Test]
public void QueueDownloadFromDownloadFinishedCallback()
{
var fileId = GenerateFileId();
var readStream = new ByteArrayStream(128 * 1024);
var client = new Client(KTestHost, TestPort);
client.Connect();
client.BeginTransaction(fileId);
client.Upload(FileType.Asset, readStream);
client.EndTransaction();
Thread.Sleep(50); // give the server a little time to finish the transaction
var targetFile1 = Path.GetTempFileName();
var downloadItem1 = new FileDownloadItem(fileId, FileType.Asset, targetFile1);
var targetFile2 = Path.GetTempFileName();
var downloadItem2 = new FileDownloadItem(fileId, FileType.Asset, targetFile2);
var mre = new ManualResetEvent(false);
Exception err = null;
client.DownloadFinished += (sender1, args1) =>
{
try
{
Assert.AreEqual(DownloadResult.Success, args1.Result);
Assert.AreEqual(args1.DownloadItem.Id, fileId);
Assert.IsTrue(File.Exists(targetFile1));
var fileBytes = File.ReadAllBytes(targetFile1);
Assert.IsTrue(Util.ByteArraysAreEqual(readStream.BackingBuffer, fileBytes));
}
catch (Exception e)
{
err = e;
}
finally
{
if (File.Exists(targetFile1))
File.Delete(targetFile1);
client.ResetDownloadFinishedEventHandler();
client.DownloadFinished += (sender2, args2) =>
{
try
{
Assert.AreEqual(DownloadResult.Success, args2.Result);
Assert.AreEqual(args2.DownloadItem.Id, fileId);
Assert.IsTrue(File.Exists(targetFile2));
var fileBytes = File.ReadAllBytes(targetFile2);
Assert.IsTrue(Util.ByteArraysAreEqual(readStream.BackingBuffer, fileBytes));
}
catch (Exception e)
{
err = e;
}
finally
{
if (File.Exists(targetFile2))
File.Delete(targetFile2);
mre.Set();
}
};
client.QueueDownload(downloadItem2);
}
};
client.QueueDownload(downloadItem1);
Assert.IsTrue(mre.WaitOne(2000));
if (err != null)
throw err;
}
[Test]
public void Connect()
{
var client = new Client(KTestHost, TestPort);
client.Connect();
client.Close();
}
[Test]
public void ConnectTimeout()
{
var client = new Client(KInvalidTestHost, TestPort);
TimeoutException err = null;
try
{
client.Connect(0);
}
catch (TimeoutException e)
{
err = e;
}
finally
{
client.Close();
Debug.Assert(err != null);
}
}
[Test]
public void TransactionIsolation()
{
var fileId = GenerateFileId();
var readStream = new ByteArrayStream(16 * 1024);
var client = new Client(KTestHost, TestPort);
client.Connect();
Assert.Throws<TransactionIsolationException>(() => client.Upload(FileType.Asset, readStream));
Assert.Throws<TransactionIsolationException>(() => client.EndTransaction());
// Back-to-back begin transactions are allowed
client.BeginTransaction(fileId);
Assert.DoesNotThrow(() => client.BeginTransaction(fileId));
}
[Test]
public void UploadDownloadOne()
{
var fileId = GenerateFileId();
var readStream = new ByteArrayStream(16 * 1024);
var client = new Client(KTestHost, TestPort);
client.Connect();
client.BeginTransaction(fileId);
client.Upload(FileType.Asset, readStream);
client.EndTransaction();
Thread.Sleep(50); // give the server a little time to finish the transaction
var downloadItem = new TestDownloadItem(fileId, FileType.Asset);
client.QueueDownload(downloadItem);
Exception err = null;
var mre = new ManualResetEvent(false);
client.DownloadFinished += (sender, args) =>
{
try
{
Assert.AreEqual(0, args.DownloadQueueLength);
Assert.AreEqual(DownloadResult.Success, args.Result);
Assert.AreEqual(fileId, args.DownloadItem.Id);
}
catch (Exception e)
{
err = e;
}
finally
{
mre.Set();
}
};
Assert.IsTrue(mre.WaitOne(2000));
if (err != null)
throw err;
Assert.IsTrue(Util.ByteArraysAreEqual(readStream.BackingBuffer, downloadItem.Bytes));
}
[Test]
public void DownloadMany()
{
const int fileCount = 5;
var fileIds = new FileId[fileCount];
var fileStreams = new ByteArrayStream[fileCount];
var client = new Client(KTestHost, TestPort);
client.Connect();
// Upload files
var rand = new Random();
for (var i = 0; i < fileCount; i++)
{
fileIds[i] = GenerateFileId();
fileStreams[i] = new ByteArrayStream(rand.Next(64 * 1024, 128 * 1024));
client.BeginTransaction(fileIds[i]);
client.Upload(FileType.Asset, fileStreams[i]);
client.EndTransaction();
}
Thread.Sleep(50);
// Download
var receivedCount = 0;
Exception err = null;
var mre = new ManualResetEvent(false);
client.DownloadFinished += (sender, args) =>
{
try
{
Assert.AreEqual(args.Result, DownloadResult.Success);
Assert.AreEqual(args.DownloadItem.Id, fileIds[receivedCount]);
var downloadItem = (TestDownloadItem)args.DownloadItem;
Assert.IsTrue(Util.ByteArraysAreEqual(fileStreams[receivedCount].BackingBuffer, downloadItem.Bytes));
receivedCount++;
Assert.AreEqual(fileCount - receivedCount, args.DownloadQueueLength);
}
catch (Exception e)
{
err = e;
}
finally
{
if (err != null || receivedCount == fileCount)
mre.Set();
}
};
for (var i = 0; i < fileCount; i++)
client.QueueDownload(new TestDownloadItem(fileIds[i], FileType.Asset));
Assert.AreEqual(fileCount, client.DownloadQueueLength);
Assert.IsTrue(mre.WaitOne(2000));
if (err != null)
throw err;
Assert.AreEqual(fileCount, receivedCount);
}
[Test]
public void DonwloadFileNotFound()
{
var client = new Client(KTestHost, TestPort);
client.Connect();
var fileId = FileId.From(new byte[16], new byte[16]);
var mre = new ManualResetEvent(false);
var downloadItem = new TestDownloadItem(fileId, FileType.Asset);
client.QueueDownload(downloadItem);
Exception err = null;
client.DownloadFinished += (sender, args) =>
{
try
{
Assert.AreEqual(args.Result, DownloadResult.FileNotFound);
Assert.AreEqual(args.DownloadItem.Id, fileId);
}
catch (Exception e)
{
err = e;
}
finally
{
mre.Set();
}
};
mre.WaitOne(500);
if (err != null)
throw err;
}
[Test]
public void ResetDownloadFinishedEventHandler()
{
var fileId = GenerateFileId();
var readStream = new ByteArrayStream(16 * 1024);
var client = new Client(KTestHost, TestPort);
client.Connect();
client.BeginTransaction(fileId);
client.Upload(FileType.Asset, readStream);
client.EndTransaction();
Thread.Sleep(50);
var downloadItem = new TestDownloadItem(fileId, FileType.Asset);
// Add two listeners that will assert if called
client.DownloadFinished += (sender, args) => { Debug.Assert(false); };
client.DownloadFinished += (sender, args) => { Debug.Assert(false); };
// Clear the listeners so they will not be called
client.ResetDownloadFinishedEventHandler();
client.QueueDownload(downloadItem);
var mre = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(state =>
{
while (client.DownloadQueueLength > 0)
Thread.Sleep(0);
mre.Set();
});
Assert.IsTrue(mre.WaitOne(2000));
}
static void WriteBytesToStream(Stream stream, byte[] buffer, int offset, int count)
{
// Write to stream, and reset position for read
stream.Write(buffer, offset, count);
stream.Position -= count;
}
[Test]
public void ReadHeader_AppendsDataToBuffer()
{
using (var stream = new MemoryStream())
{
var client = new Client(KTestHost, TestPort);
client.m_stream = stream;
Exception err = null;
var resetEvent = new ManualResetEvent(false);
var testBuffer = new byte[] { (byte)'-', (byte)'0' };
WriteBytesToStream(stream, testBuffer, (int)stream.Length, 1);
// Setup checks / callback
client.OnReadHeader += (bytesRead, readBuffer) =>
{
try
{
// Validate written and read bytes match
Assert.AreEqual((int)stream.Length, bytesRead);
// Validate buffer contents are correct up to the current written bytes
for (int i = 0; i < (int)stream.Length; i++)
Assert.AreEqual(testBuffer[i], readBuffer[i]);
if ((int)stream.Length < testBuffer.Length)
WriteBytesToStream(stream, testBuffer, (int)stream.Length, 1);
else
resetEvent.Set();
}
catch (Exception e)
{
err = e;
resetEvent.Set();
}
};
// Run test
client.ReadNextDownloadResult();
Assert.IsTrue(resetEvent.WaitOne(2000));
if (err != null)
throw err;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 012e8bc53e96d4c638db9f5b04ef84de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,17 @@
{
"name": "UnityEditor.CacheServerTests",
"references": [
"UnityEditor.CacheServer"
],
"includePlatforms": [
"Editor"
],
"optionalUnityReferences": [
"TestAssemblies"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
]
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: de0adf19d0a2a46a19acaa82fcb5a988
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,24 @@
using System;
using System.IO;
namespace UnityEditor.CacheServerTests
{
public static class Utils
{
public static class Paths
{
public static string Combine(params string[] components)
{
if (components.Length < 1)
throw new ArgumentException("At least one component must be provided!");
var path1 = components[0];
for (var index = 1; index < components.Length; ++index)
path1 = Path.Combine(path1, components[index]);
return path1;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 493e67a6e86284a8681aab6f10d05c34
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,33 @@
using System.IO;
using NUnit.Framework;
using UnityEditor.Build.Pipeline.Utilities;
namespace UnityEditor.Build.Pipeline.Tests
{
// Test suite against Build Cache with Cache Server backend, tests are implemented in BuildCacheTestBase
//[Ignore("Disabled due to instability")]
//class CacheServerBuildCacheTests : BuildCacheTestBase
//{
// internal override void OneTimeSetupDerived()
// {
// var cachePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
// LocalCacheServer.Setup(1024 * 1024, cachePath);
// }
// internal override void OneTimeTearDownDerived()
// {
// LocalCacheServer.Clear();
// }
// protected override void RecreateBuildCache()
// {
// // purge the local cache to make sure we don't load anything out of it again.
// // these tests need to pull from the cache server
// BuildCacheUtility.ClearCacheHashes();
// if (m_Cache != null)
// m_Cache.Dispose();
// PurgeBuildCache();
// m_Cache = new BuildCache("localhost", LocalCacheServer.instance.m_port);
// }
//}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cde708facb1abd24cb7b42fedf436309
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,659 @@
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline;
using UnityEditor.Build.Pipeline.Injector;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEditor.Build.Player;
using UnityEditor.U2D;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.U2D;
namespace UnityEditor.Build.Pipeline.Tests
{
public class CalculateAssetDependencyTests
{
const string kTestAssetFolder = "Assets/TestAssets";
const string kTestAsset = "Assets/TestAssets/SpriteTexture32x32.png";
const string kSpriteTexture1Asset = "Assets/TestAssets/SpriteTexture1_32x32.png";
const string kSpriteTexture2Asset = "Assets/TestAssets/SpriteTexture2_32x32.png";
const string kSpriteAtlasAsset = "Assets/TestAssets/sadependencies.spriteAtlas";
SpritePackerMode m_PrevMode;
class TestParams : TestBundleBuildParameters
{
bool m_DisableVisibleSubAssetRepresentations = false;
public TestParams(bool disableVisibleSubAssetRepresentations)
{
m_DisableVisibleSubAssetRepresentations = disableVisibleSubAssetRepresentations;
}
public override bool DisableVisibleSubAssetRepresentations { get => m_DisableVisibleSubAssetRepresentations; }
// Inputs
public override bool UseCache { get; set; }
public override BuildTarget Target { get => EditorUserBuildSettings.activeBuildTarget; }
public override BuildTargetGroup Group { get => BuildTargetGroup.Unknown; }
public override TypeDB ScriptInfo { get => null; }
public override ContentBuildFlags ContentBuildFlags { get => ContentBuildFlags.None; }
public override bool NonRecursiveDependencies { get => false; }
#if !UNITY_2019_3_OR_NEWER
public override string TempOutputFolder => ContentPipeline.kTempBuildPath;
#endif
public override BuildSettings GetContentBuildSettings()
{
return new BuildSettings
{
group = Group,
target = Target,
typeDB = ScriptInfo,
buildFlags = ContentBuildFlags
};
}
}
class TestContent : TestBundleBuildContent
{
public List<GUID> TestAssets = new List<GUID>();
// Inputs
public override List<GUID> Assets => TestAssets;
}
class TestDependencyData : TestDependencyDataBase
{
public Dictionary<GUID, AssetLoadInfo> TestAssetInfo = new Dictionary<GUID, AssetLoadInfo>();
public Dictionary<GUID, BuildUsageTagSet> TestAssetUsage = new Dictionary<GUID, BuildUsageTagSet>();
public Dictionary<GUID, SceneDependencyInfo> TestSceneInfo = new Dictionary<GUID, SceneDependencyInfo>();
public Dictionary<GUID, BuildUsageTagSet> TestSceneUsage = new Dictionary<GUID, BuildUsageTagSet>();
public Dictionary<GUID, Hash128> TestDependencyHash = new Dictionary<GUID, Hash128>();
// Inputs
public override BuildUsageCache DependencyUsageCache => null;
public override BuildUsageTagGlobal GlobalUsage => new BuildUsageTagGlobal();
// Outputs
public override Dictionary<GUID, AssetLoadInfo> AssetInfo => TestAssetInfo;
public override Dictionary<GUID, BuildUsageTagSet> AssetUsage => TestAssetUsage;
public override Dictionary<GUID, SceneDependencyInfo> SceneInfo => TestSceneInfo;
public override Dictionary<GUID, BuildUsageTagSet> SceneUsage => TestSceneUsage;
public override Dictionary<GUID, Hash128> DependencyHash => TestDependencyHash;
}
class TestExtendedAssetData : TestBundleExtendedAssetData
{
public Dictionary<GUID, ExtendedAssetData> TestExtendedData = new Dictionary<GUID, ExtendedAssetData>();
public override Dictionary<GUID, ExtendedAssetData> ExtendedData => TestExtendedData;
}
static CalculateAssetDependencyData CreateDefaultBuildTask(List<GUID> assets, bool disableVisibleSubassetRepresentations = false)
{
var task = new CalculateAssetDependencyData();
var testParams = new TestParams(disableVisibleSubassetRepresentations);
var testContent = new TestContent { TestAssets = assets };
var testDepData = new TestDependencyData();
var testExtendedData = new TestExtendedAssetData();
var testObjDependencyData = new ObjectDependencyData();
IBuildContext context = new BuildContext(testParams, testContent, testDepData, testExtendedData, testObjDependencyData);
ContextInjector.Inject(context, task);
return task;
}
static void ExtractTestData(IBuildTask task, out TestExtendedAssetData extendedAssetData)
{
IBuildContext context = new BuildContext();
ContextInjector.Extract(context, task);
extendedAssetData = (TestExtendedAssetData)context.GetContextObject<IBuildExtendedAssetData>();
}
[SetUp]
public void Setup()
{
m_PrevMode = EditorSettings.spritePackerMode;
Directory.CreateDirectory(kTestAssetFolder);
CreateTestSpriteTexture(kTestAsset, false);
CreateTestSpriteTexture(kSpriteTexture1Asset, true);
CreateTestSpriteTexture(kSpriteTexture2Asset, true);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate);
CreateSpriteAtlas();
}
[TearDown]
public void OneTimeTeardown()
{
AssetDatabase.DeleteAsset(kTestAssetFolder);
File.Delete(kTestAssetFolder + ".meta");
EditorSettings.spritePackerMode = m_PrevMode;
AssetDatabase.Refresh();
}
static void CreateTestSpriteTexture(string texturePath, bool single)
{
var data = ImageConversion.EncodeToPNG(new Texture2D(32, 32));
File.WriteAllBytes(texturePath, data);
AssetDatabase.ImportAsset(texturePath);//, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate);
var importer = AssetImporter.GetAtPath(texturePath) as TextureImporter;
importer.textureType = TextureImporterType.Sprite;
if (single)
{
importer.spriteImportMode = SpriteImportMode.Single;
}
else
{
importer.spriteImportMode = SpriteImportMode.Multiple;
importer.spritesheet = new[]
{
new SpriteMetaData
{
name = "WhiteTexture32x32_0",
rect = new Rect(0, 19, 32, 13),
alignment = 0,
pivot = new Vector2(0.5f, 0.5f),
border = new Vector4(0, 0, 0, 0)
},
new SpriteMetaData
{
name = "WhiteTexture32x32_1",
rect = new Rect(4, 19, 24, 11),
alignment = 0,
pivot = new Vector2(0.5f, 0.5f),
border = new Vector4(0, 0, 0, 0)
},
new SpriteMetaData
{
name = "WhiteTexture32x32_2",
rect = new Rect(9, 5, 12, 7),
alignment = 0,
pivot = new Vector2(0.5f, 0.5f),
border = new Vector4(0, 0, 0, 0)
}
};
}
importer.SaveAndReimport();
}
static void CreateSpriteAtlas()
{
var sa = new SpriteAtlas();
var targetObjects = new UnityEngine.Object[] { AssetDatabase.LoadAssetAtPath<Texture>(kSpriteTexture1Asset), AssetDatabase.LoadAssetAtPath<Texture>(kSpriteTexture2Asset) };
sa.Add(targetObjects);
AssetDatabase.CreateAsset(sa, kSpriteAtlasAsset);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate);
}
CalculateAssetDependencyData.TaskInput CreateDefaultInput()
{
CalculateAssetDependencyData.TaskInput input = new CalculateAssetDependencyData.TaskInput();
input.Target = EditorUserBuildSettings.activeBuildTarget;
return input;
}
// Create a prefab and writes it to the specified path. The target file will have 2 objects in it: the GameObject and the Transform
GUID CreateGameObject(string assetPath, string name = "go")
{
GameObject go = new GameObject(name);
PrefabUtility.SaveAsPrefabAsset(go, assetPath);
UnityEngine.Object.DestroyImmediate(go, false);
string guidString = AssetDatabase.AssetPathToGUID(assetPath);
GUID.TryParse(guidString, out GUID guid);
return guid;
}
[Test]
public void WhenAssetHasNoDependencies()
{
// Create an asset
string assetPath = Path.Combine(kTestAssetFolder, "myPrefab.prefab");
GUID guid = CreateGameObject(assetPath);
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
input.Assets = new List<GUID>() { guid };
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
Assert.AreEqual(1, output.AssetResults.Length);
Assert.AreEqual(guid, output.AssetResults[0].asset);
Assert.AreEqual(2, output.AssetResults[0].assetInfo.includedObjects.Count); // GameObject and Transform
Assert.AreEqual(2, output.AssetResults[0].objectDependencyInfo.Count);
Assert.AreEqual(0, output.AssetResults[0].assetInfo.referencedObjects.Count);
Assert.IsNull(output.AssetResults[0].spriteData);
Assert.IsNull(output.AssetResults[0].extendedData);
}
[Test]
public void WhenAssetDoesNotExist_AssetResultIsEmpty()
{
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
input.Assets = new List<GUID>() { GUID.Generate() };
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
Assert.IsNull(output.AssetResults[0].extendedData);
Assert.AreEqual(0, output.AssetResults[0].assetInfo.includedObjects.Count);
Assert.AreEqual(0, output.AssetResults[0].objectDependencyInfo.Count);
}
[Test]
public void WhenSomeAssetDataIsCached_CachedVersionIsUsed()
{
const int kCachedCount = 5;
// Create 10 assets, import half of them,
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
string assetPath = Path.Combine(kTestAssetFolder, "myPrefab.prefab");
List<GUID> allGUIDs = new List<GUID>();
List<GUID> cachedGUIDs = new List<GUID>();
for (int i = 0; i < kCachedCount; i++)
{
GUID cachedGUID = CreateGameObject(Path.Combine(kTestAssetFolder, $"myPrefab{i * 2}.prefab"), $"go{i * 2}");
cachedGUIDs.Add(cachedGUID);
allGUIDs.Add(cachedGUID);
allGUIDs.Add(CreateGameObject(Path.Combine(kTestAssetFolder, $"myPrefab{i * 2 + 1}.prefab"), $"go{i * 2 + 1}"));
}
using (BuildCache cache = new BuildCache())
{
cache.ClearCacheEntryMaps(); // needed if running multiple times
input.BuildCache = cache;
input.Assets = cachedGUIDs;
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
cache.SyncPendingSaves();
input.Assets = allGUIDs;
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output2);
Assert.AreEqual(output.AssetResults[0].assetInfo.includedObjects.Count, output2.AssetResults[0].assetInfo.includedObjects.Count); // GameObject and Transform
Assert.AreEqual(output.AssetResults[0].objectDependencyInfo.Count, output2.AssetResults[0].objectDependencyInfo.Count);
Assert.AreEqual(0, output.CachedAssetCount);
Assert.AreEqual(kCachedCount, output2.CachedAssetCount);
for (int i = 0; i < kCachedCount; i++)
{
bool seqEqual = Enumerable.SequenceEqual(output.AssetResults[i].assetInfo.includedObjects, output2.AssetResults[i * 2].assetInfo.includedObjects);
Assert.IsTrue(seqEqual, "Included Objects where not the same when Cache was used.");
Assert.AreEqual(output.AssetResults[i].objectDependencyInfo.Count, output2.AssetResults[i * 2].objectDependencyInfo.Count);
for (int o = 0; o < output.AssetResults[i].objectDependencyInfo.Count; ++o)
{
var left = output.AssetResults[i].objectDependencyInfo[o];
bool found = false;
foreach (ObjectDependencyInfo right in output2.AssetResults[i * 2].objectDependencyInfo)
{
if (right.Object == left.Object)
{
Assert.AreEqual(right.Dependencies.Count, left.Dependencies.Count, "Object dependency found between builds, but the dependencies are different count");
found = true;
break;
}
}
Assert.IsTrue(found, "Could not find matching object dependency between builds");
}
}
}
}
// Embedding this shader in code and only creating it when the test actually runs so it doesn't exist outside tests.
string kTestShader = @"Shader ""Custom / NewSurfaceShader""
{
Properties
{
_Color(""Color"", Color) = (1, 1, 1, 1)
_MainTex(""Albedo (RGB)"", 2D) = ""white"" {
}
_Glossiness(""Smoothness"", Range(0, 1)) = 0.5
_Metallic(""Metallic"", Range(0, 1)) = 0.0
}
SubShader
{
Tags { ""RenderType"" = ""Opaque"" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
#pragma shader_feature _ TEST_DEFINE
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)
void surf(Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack ""Diffuse""
}
";
private void CreateTestShader(string path)
{
if (!File.Exists(path))
{
File.WriteAllText(path, kTestShader);
AssetDatabase.Refresh();
}
}
[Test]
public void WhenObjectInfluencesReferencedObjectBuildTags_BuildUsageTagsAreAdded()
{
string testShaderPath = Path.Combine(kTestAssetFolder, "TestShader.shader");
CreateTestShader(testShaderPath);
Shader shader = AssetDatabase.LoadAssetAtPath<Shader>(testShaderPath);
string shaderGUIDString = AssetDatabase.AssetPathToGUID(testShaderPath);
GUID.TryParse(shaderGUIDString, out GUID shaderGUID);
// Create a material that points to the test shader asset
Material mat = new Material(shader);
string matPath = Path.Combine(kTestAssetFolder, "testmat.mat");
AssetDatabase.CreateAsset(mat, matPath);
string guidMatString = AssetDatabase.AssetPathToGUID(matPath);
GUID.TryParse(guidMatString, out GUID matGUID);
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
input.Assets = new List<GUID>() { matGUID };
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
// this define should get added to the shader build usage tags
mat.shaderKeywords = new string[] { "TEST_DEFINE" };
CalculateAssetDependencyData.TaskInput input2 = CreateDefaultInput();
input2.Assets = new List<GUID>() { matGUID };
CalculateAssetDependencyData.RunInternal(input2, out CalculateAssetDependencyData.TaskOutput output2);
var ids = output2.AssetResults[0].usageTags.GetObjectIdentifiers();
Assert.IsTrue(ids.Count((x) => x.guid == shaderGUID) == 1, "Shader is not in build usage tags");
Assert.AreNotEqual(output.AssetResults[0].usageTags.GetHashCode(), output2.AssetResults[0].usageTags.GetHashCode(), "Build usage tags were not affected by material keywords");
}
static object[] SpriteTestCases =
{
#if UNITY_2020_1_OR_NEWER
new object[] { SpritePackerMode.Disabled, "", false, false },
new object[] { SpritePackerMode.BuildTimeOnlyAtlas, "", true, true },
new object[] { SpritePackerMode.BuildTimeOnlyAtlas, "", false, false },
#else
new object[] { SpritePackerMode.BuildTimeOnly, "SomeTag", true, true },
new object[] { SpritePackerMode.BuildTimeOnly, "", true, false },
new object[] { SpritePackerMode.AlwaysOn, "SomeTag", true, true },
new object[] { SpritePackerMode.AlwaysOn, "", true, false },
new object[] { SpritePackerMode.Disabled, "", true, false },
new object[] { SpritePackerMode.BuildTimeOnlyAtlas, "", true, true },
new object[] { SpritePackerMode.AlwaysOnAtlas, "", true, true },
new object[] { SpritePackerMode.AlwaysOnAtlas, "", false, false }
#endif
};
[TestCaseSource("SpriteTestCases")]
[Test]
public void WhenSpriteWithAtlas_SpriteImportDataCreated(SpritePackerMode spriteMode, string spritePackingTag, bool hasReferencingSpriteAtlas, bool expectedPacked)
{
TextureImporter importer = AssetImporter.GetAtPath(kTestAsset) as TextureImporter;
importer.spritePackingTag = spritePackingTag;
importer.SaveAndReimport();
if (hasReferencingSpriteAtlas)
{
var sa = new SpriteAtlas();
var targetObjects = new UnityEngine.Object[] { AssetDatabase.LoadAssetAtPath<Texture>(kTestAsset) };
sa.Add(targetObjects);
string saPath = Path.Combine(kTestAssetFolder, "sa.spriteAtlas");
AssetDatabase.CreateAsset(sa, saPath);
AssetDatabase.Refresh();
}
GUID.TryParse(AssetDatabase.AssetPathToGUID(kTestAsset), out GUID spriteGUID);
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
EditorSettings.spritePackerMode = spriteMode;
SpriteAtlasUtility.PackAllAtlases(input.Target);
input.Assets = new List<GUID>() { spriteGUID };
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
Assert.AreEqual(expectedPacked, output.AssetResults[0].spriteData.PackedSprite);
}
static object[] SpriteUtilityTestCases =
{
new object[] { SpritePackerMode.BuildTimeOnlyAtlas }
};
[TestCaseSource("SpriteUtilityTestCases")]
[Test]
public void WhenSpriteWithAtlasUpdated_SpriteInfoUpdated(SpritePackerMode spriteMode)
{
var spriteAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(kSpriteAtlasAsset);
var oldSprites = SpriteAtlasExtensions.GetPackables(spriteAtlas);
spriteAtlas.Remove(oldSprites);
List<UnityEngine.Object> sprites = new List<UnityEngine.Object>();
sprites.Add(AssetDatabase.LoadAssetAtPath<Sprite>(kSpriteTexture1Asset));
sprites.Add(AssetDatabase.LoadAssetAtPath<Sprite>(kSpriteTexture2Asset));
SpriteAtlasExtensions.Add(spriteAtlas, sprites.ToArray());
GUID.TryParse(AssetDatabase.AssetPathToGUID(kSpriteAtlasAsset), out GUID spriteGUID);
BuildCacheUtility.ClearCacheHashes();
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
input.BuildCache = new BuildCache();
input.NonRecursiveDependencies = true;
EditorSettings.spritePackerMode = spriteMode;
SpriteAtlasUtility.PackAllAtlases(input.Target);
input.Assets = new List<GUID>() { spriteGUID };
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
Assert.AreEqual(3, output.AssetResults[0].assetInfo.referencedObjects.Count);
spriteAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(kSpriteAtlasAsset);
oldSprites = SpriteAtlasExtensions.GetPackables(spriteAtlas);
spriteAtlas.Remove(oldSprites);
sprites = new List<UnityEngine.Object>();
sprites.Add(AssetDatabase.LoadAssetAtPath<Sprite>(kSpriteTexture1Asset));
SpriteAtlasExtensions.Add(spriteAtlas, sprites.ToArray());
SpriteAtlasUtility.PackAllAtlases(input.Target);
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output2);
Assert.AreEqual(2, output2.AssetResults[0].assetInfo.referencedObjects.Count);
}
#if !UNITY_2020_2_OR_NEWER
// This test is only important for going through AssetDatabase's LoadAllAssetRepresentationsAtPath
// in 2020.2 and newer we have a new build api that handles nulls natively and this no longer applies.
class NullLoadRepresentationFake : CalculateAssetDependencyHooks
{
public override UnityEngine.Object[] LoadAllAssetRepresentationsAtPath(string assetPath) { return new UnityEngine.Object[] { null }; }
}
[Test]
public void WhenAssetHasANullRepresentation_LogsWarning()
{
// Create an asset
string assetPath = Path.Combine(kTestAssetFolder, "myPrefab.prefab");
GUID guid = CreateGameObject(assetPath);
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
input.Assets = new List<GUID>() { guid };
input.EngineHooks = new NullLoadRepresentationFake();
LogAssert.Expect(LogType.Warning, new Regex(".+It will not be included in the build"));
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
Assert.AreEqual(guid, output.AssetResults[0].asset);
Assert.AreEqual(2, output.AssetResults[0].assetInfo.includedObjects.Count); // GameObject and Transform
Assert.AreEqual(0, output.AssetResults[0].assetInfo.referencedObjects.Count);
}
#endif
[Test]
public void WhenAssetHasMultipleRepresentations_ExtendedDataContainsAllButMainAsset()
{
const int kExtraRepresentations = 2;
string assetPath = Path.Combine(kTestAssetFolder, "myPrefab.asset");
Material mat = new Material(Shader.Find("Transparent/Diffuse"));
AssetDatabase.CreateAsset(mat, assetPath);
for (int i = 0; i < kExtraRepresentations; i++)
AssetDatabase.AddObjectToAsset(new Material(Shader.Find("Transparent/Diffuse")), assetPath);
AssetDatabase.SaveAssets();
GUID guid = new GUID(AssetDatabase.AssetPathToGUID(assetPath));
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
input.Assets = new List<GUID>() { guid };
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
ObjectIdentifier[] allObjIDs = ContentBuildInterface.GetPlayerObjectIdentifiersInAsset(guid, EditorUserBuildSettings.activeBuildTarget);
HashSet<ObjectIdentifier> expectedReps = new HashSet<ObjectIdentifier>();
for (int i = 1; i < allObjIDs.Length; i++)
expectedReps.Add(allObjIDs[i]);
Assert.AreEqual(kExtraRepresentations, output.AssetResults[0].extendedData.Representations.Count);
Assert.AreEqual(kExtraRepresentations, expectedReps.Count);
foreach (var id in output.AssetResults[0].extendedData.Representations)
Assert.IsTrue(expectedReps.Contains(id));
}
[Test]
public void WhenExplicitSpriteAndAtlas_AtlasOnlyReferencesSprites()
{
GUID spriteAtlasGuid = new GUID(AssetDatabase.AssetPathToGUID(kSpriteAtlasAsset));
GUID sprite1Guid = new GUID(AssetDatabase.AssetPathToGUID(kSpriteTexture1Asset));
GUID sprite2Guid = new GUID(AssetDatabase.AssetPathToGUID(kSpriteTexture2Asset));
EditorSettings.spritePackerMode = SpritePackerMode.BuildTimeOnlyAtlas;
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
SpriteAtlasUtility.PackAllAtlases(input.Target);
input.Assets = new List<GUID>() { spriteAtlasGuid, sprite1Guid, sprite2Guid };
CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
Assert.AreEqual(3, output.AssetResults[0].assetInfo.referencedObjects.Count);
}
class TestProgressTracker : IProgressTracker
{
int count = 0;
public int TaskCount { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public float Progress => throw new NotImplementedException();
public bool UpdateInfo(string taskInfo)
{
return count++ > 0;
}
public bool UpdateTask(string taskTitle) { throw new NotImplementedException(); }
}
[Test]
public void WhenCanceledThroughProgressTracker_ReturnsCanceled()
{
string assetPath1 = Path.Combine(kTestAssetFolder, "myPrefab1.prefab");
string assetPath2 = Path.Combine(kTestAssetFolder, "myPrefab2.prefab");
CalculateAssetDependencyData.TaskInput input = CreateDefaultInput();
input.Assets = new List<GUID>() { CreateGameObject(assetPath1), CreateGameObject(assetPath2) };
input.ProgressTracker = new TestProgressTracker();
ReturnCode code = CalculateAssetDependencyData.RunInternal(input, out CalculateAssetDependencyData.TaskOutput output);
Assert.AreEqual(null, output.AssetResults[1].assetInfo);
Assert.AreEqual(ReturnCode.Canceled, code);
}
[Test]
public void TaskIsRun_WhenAssetHasNoMultipleRepresentations_ExtendedDataIsEmpty()
{
string assetPath = Path.Combine(kTestAssetFolder, "myPrefab.prefab");
GUID guid = CreateGameObject(assetPath);
bool disableVisibleSubAssetRepresentations = false;
CalculateAssetDependencyData buildTask = CreateDefaultBuildTask(new List<GUID>() { guid }, disableVisibleSubAssetRepresentations);
buildTask.Run();
ExtractTestData(buildTask, out TestExtendedAssetData extendedAssetData);
Assert.AreEqual(0, extendedAssetData.ExtendedData.Count);
}
[Test]
public void TaskIsRun_WhenAssetHasMultipleRepresentations_ExtendedDataContainsEntryForAsset()
{
string assetPath = Path.Combine(kTestAssetFolder, "myPrefab.asset");
Material mat = new Material(Shader.Find("Transparent/Diffuse"));
AssetDatabase.CreateAsset(mat, assetPath);
AssetDatabase.AddObjectToAsset(new Material(Shader.Find("Transparent/Diffuse")), assetPath);
AssetDatabase.SaveAssets();
string guidString = AssetDatabase.AssetPathToGUID(assetPath);
GUID.TryParse(guidString, out GUID guid);
bool disableVisibleSubAssetRepresentations = false;
CalculateAssetDependencyData buildTask = CreateDefaultBuildTask(new List<GUID>() { guid }, disableVisibleSubAssetRepresentations);
buildTask.Run();
ExtractTestData(buildTask, out TestExtendedAssetData extendedAssetData);
Assert.AreEqual(1, extendedAssetData.ExtendedData.Count);
Assert.IsTrue(extendedAssetData.ExtendedData.ContainsKey(guid));
}
[Test]
public void TaskIsRun_WhenAssetHasMultipleRepresentations_AndDisableVisibleSubAssetRepresentations_ExtendedDataIsEmpty()
{
string assetPath = Path.Combine(kTestAssetFolder, "myPrefab.asset");
Material mat = new Material(Shader.Find("Transparent/Diffuse"));
AssetDatabase.CreateAsset(mat, assetPath);
AssetDatabase.AddObjectToAsset(new Material(Shader.Find("Transparent/Diffuse")), assetPath);
AssetDatabase.SaveAssets();
string guidString = AssetDatabase.AssetPathToGUID(assetPath);
GUID.TryParse(guidString, out GUID guid);
bool disableVisibleSubAssetRepresentations = true;
CalculateAssetDependencyData buildTask = CreateDefaultBuildTask(new List<GUID>() { guid }, disableVisibleSubAssetRepresentations);
buildTask.Run();
ExtractTestData(buildTask, out TestExtendedAssetData extendedAssetData);
Assert.AreEqual(0, extendedAssetData.ExtendedData.Count);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 155a61777399d7f48800d1791751daab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,214 @@
#if UNITY_2019_3_OR_NEWER
using NUnit.Framework;
using System.Collections.Generic;
using System.Reflection;
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.Player;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
public class CalculateCustomDependencyTests
{
class TestBuildParameters : TestBuildParametersBase
{
// Optional Inputs
public override BuildTarget Target => BuildTarget.NoTarget;
public override TypeDB ScriptInfo => null;
public override bool UseCache { get => false; set => base.UseCache = value; }
}
class TestContent : TestBundleBuildContent
{
// Inputs
List<CustomContent> m_CustomAssets;
public override List<CustomContent> CustomAssets => m_CustomAssets;
// Outputs
Dictionary<string, List<GUID>> m_BundleLayout;
Dictionary<GUID, string> m_Addresses;
public override Dictionary<string, List<GUID>> BundleLayout => m_BundleLayout;
public override Dictionary<GUID, string> Addresses => m_Addresses;
public TestContent(List<CustomContent> customAssets)
{
m_CustomAssets = customAssets;
m_BundleLayout = new Dictionary<string, List<GUID>>();
m_Addresses = new Dictionary<GUID, string>();
}
}
class TestDependencyData : TestDependencyDataBase
{
// Input / Output
Dictionary<GUID, AssetLoadInfo> m_AssetInfo;
public override Dictionary<GUID, AssetLoadInfo> AssetInfo => m_AssetInfo;
// Optional Inputs
BuildUsageTagGlobal m_GlobalUsage;
Dictionary<GUID, SceneDependencyInfo> m_SceneInfo;
public override BuildUsageTagGlobal GlobalUsage { get => m_GlobalUsage; set => m_GlobalUsage = value; }
public override Dictionary<GUID, SceneDependencyInfo> SceneInfo => m_SceneInfo;
public override BuildUsageCache DependencyUsageCache => null;
// Outputs
Dictionary<GUID, BuildUsageTagSet> m_AssetUsage;
public override Dictionary<GUID, BuildUsageTagSet> AssetUsage => m_AssetUsage;
public TestDependencyData(Dictionary<GUID, AssetLoadInfo> assetInfo)
{
m_AssetInfo = assetInfo;
m_GlobalUsage = new BuildUsageTagGlobal();
m_SceneInfo = new Dictionary<GUID, SceneDependencyInfo>();
m_AssetUsage = new Dictionary<GUID, BuildUsageTagSet>();
}
}
static CalculateCustomDependencyData CreateDefaultBuildTask(List<CustomContent> customAssets, Dictionary<GUID, AssetLoadInfo> assetInfo = null)
{
var task = new CalculateCustomDependencyData();
var testParams = new TestBuildParameters();
var testContent = new TestContent(customAssets);
if (assetInfo == null)
assetInfo = new Dictionary<GUID, AssetLoadInfo>();
var testData = new TestDependencyData(assetInfo);
IBuildContext context = new BuildContext(testParams, testContent, testData);
ContextInjector.Inject(context, task);
return task;
}
static void ExtractTestData(IBuildTask task, out CustomAssets customAssets, out TestContent content, out TestDependencyData dependencyData)
{
IBuildContext context = new BuildContext();
ContextInjector.Extract(context, task);
customAssets = (CustomAssets)context.GetContextObject<ICustomAssets>();
content = (TestContent)context.GetContextObject<IBundleBuildContent>();
dependencyData = (TestDependencyData)context.GetContextObject<IDependencyData>();
}
static ObjectIdentifier MakeObjectId(string guid, long localIdentifierInFile, FileType fileType, string filePath)
{
var objectId = new ObjectIdentifier();
var boxed = (object)objectId;
var type = typeof(ObjectIdentifier);
type.GetField("m_GUID", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, new GUID(guid));
type.GetField("m_LocalIdentifierInFile", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, localIdentifierInFile);
type.GetField("m_FileType", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, fileType);
type.GetField("m_FilePath", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, filePath);
return (ObjectIdentifier)boxed;
}
[Test]
public void CreateAssetEntryForObjectIdentifiers_ThrowsExceptionOnAssetGUIDCollision()
{
var assetPath = "temp/test_serialized_file.asset";
UnityEditorInternal.InternalEditorUtility.SaveToSerializedFileAndForget(new[] { Texture2D.whiteTexture, Texture2D.redTexture }, assetPath, false);
var address = "CustomAssetAddress";
var assetInfo = new Dictionary<GUID, AssetLoadInfo>();
assetInfo.Add(HashingMethods.Calculate(address).ToGUID(), new AssetLoadInfo());
var customContent = new List<CustomContent>
{
new CustomContent
{
Asset = new GUID(),
Processor = (guid, task) =>
{
task.GetObjectIdentifiersAndTypesForSerializedFile(assetPath, out var objectIdentifiers, out var types);
var ex = Assert.Throws<System.ArgumentException>(() => task.CreateAssetEntryForObjectIdentifiers(objectIdentifiers, assetPath, "CustomAssetBundle", address, types[0]));
var expected = string.Format("Custom Asset '{0}' already exists. Building duplicate asset entries is not supported.", address);
Assert.That(ex.Message, Is.EqualTo(expected));
}
}
};
var buildTask = CreateDefaultBuildTask(customContent, assetInfo);
buildTask.Run();
}
[Test]
public void GetObjectIdentifiersAndTypesForSerializedFile_ReturnsAllObjectIdentifiersAndTypes()
{
var assetPath = "temp/test_serialized_file.asset";
UnityEditorInternal.InternalEditorUtility.SaveToSerializedFileAndForget(new[] { Texture2D.whiteTexture, Texture2D.redTexture }, assetPath, false);
var customContent = new List<CustomContent>
{
new CustomContent
{
Asset = new GUID(),
Processor = (guid, task) =>
{
task.GetObjectIdentifiersAndTypesForSerializedFile(assetPath, out var objectIdentifiers, out var types);
Assert.AreEqual(2, objectIdentifiers.Length);
Assert.AreEqual(MakeObjectId("00000000000000000000000000000000", 1, FileType.NonAssetType, assetPath), objectIdentifiers[0]);
Assert.AreEqual(MakeObjectId("00000000000000000000000000000000", 2, FileType.NonAssetType, assetPath), objectIdentifiers[1]);
Assert.AreEqual(1, types.Length);
Assert.AreEqual(typeof(Texture2D), types[0]);
}
}
};
var buildTask = CreateDefaultBuildTask(customContent);
buildTask.Run();
}
[Test]
public void CreateAssetEntryForObjectIdentifiers_AddsNewBundleAndAssetDataForCustomAsset()
{
var assetPath = "temp/test_serialized_file.asset";
var bundleName = "CustomAssetBundle";
var address = "CustomAssetAddress";
var assetGuid = HashingMethods.Calculate(address).ToGUID();
UnityEditorInternal.InternalEditorUtility.SaveToSerializedFileAndForget(new[] { Texture2D.whiteTexture, Texture2D.redTexture }, assetPath, false);
var customContent = new List<CustomContent>
{
new CustomContent
{
Asset = new GUID(),
Processor = (guid, task) =>
{
task.GetObjectIdentifiersAndTypesForSerializedFile(assetPath, out var includedObjects, out var types);
task.CreateAssetEntryForObjectIdentifiers(includedObjects, assetPath, bundleName, address, types[0]);
}
}
};
var buildTask = CreateDefaultBuildTask(customContent);
buildTask.Run();
ExtractTestData(buildTask, out var customAssets, out var content, out var dependencyData);
// Ensure the bundle name was added, and the custom asset guid was added to that bundle
Assert.IsTrue(content.BundleLayout.ContainsKey(bundleName));
CollectionAssert.Contains(content.BundleLayout[bundleName], assetGuid);
// Ensure the custom address was added
Assert.IsTrue(content.Addresses.ContainsKey(assetGuid));
Assert.AreEqual(address, content.Addresses[assetGuid]);
// Ensure AssetInfo contains the calculated includes and references for the custom asset
Assert.IsTrue(dependencyData.AssetInfo.ContainsKey(assetGuid));
var loadInfo = dependencyData.AssetInfo[assetGuid];
Assert.AreEqual(address, loadInfo.address);
Assert.AreEqual(assetGuid, loadInfo.asset);
Assert.AreEqual(2, loadInfo.includedObjects.Count);
Assert.AreEqual(0, loadInfo.referencedObjects.Count);
// Ensure the usage tags were added
Assert.IsTrue(dependencyData.AssetUsage.ContainsKey(assetGuid));
Assert.IsNotNull(dependencyData.AssetUsage[assetGuid]);
// Ensure the custom asset was registered in the customAssets list
CollectionAssert.Contains(customAssets.Assets, assetGuid);
}
}
}
#endif

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba3cb7b4776e6c24ba53e7f8f7e751b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.IO;
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.Player;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace UnityEditor.Build.Pipeline.Tests
{
public class CalculateSceneDependencyTests
{
class TestParams : TestBuildParametersBase
{
// Inputs
public override bool UseCache { get; set; }
public override BuildTarget Target { get => BuildTarget.NoTarget; }
public override BuildTargetGroup Group { get => BuildTargetGroup.Unknown; }
public override TypeDB ScriptInfo { get => null; }
public override ContentBuildFlags ContentBuildFlags { get => ContentBuildFlags.None; }
public override bool NonRecursiveDependencies { get; set; }
#if !UNITY_2019_3_OR_NEWER
public override string TempOutputFolder => ContentPipeline.kTempBuildPath;
#endif
public override BuildSettings GetContentBuildSettings()
{
return new BuildSettings
{
group = Group,
target = Target,
typeDB = ScriptInfo,
buildFlags = ContentBuildFlags
};
}
}
class TestContent : TestBundleBuildContent
{
public List<GUID> TestScenes = new List<GUID>();
public List<GUID> TestAssets = new List<GUID>();
// Inputs
public override List<GUID> Scenes => TestScenes;
public override List<GUID> Assets => TestAssets;
}
class TestDependencyData : TestDependencyDataBase
{
public Dictionary<GUID, SceneDependencyInfo> TestSceneInfo = new Dictionary<GUID, SceneDependencyInfo>();
public Dictionary<GUID, BuildUsageTagSet> TestSceneUsage = new Dictionary<GUID, BuildUsageTagSet>();
public Dictionary<GUID, Hash128> TestDependencyHash = new Dictionary<GUID, Hash128>();
// Inputs
public override BuildUsageCache DependencyUsageCache => null;
// Outputs
public override Dictionary<GUID, SceneDependencyInfo> SceneInfo => TestSceneInfo;
public override Dictionary<GUID, BuildUsageTagSet> SceneUsage => TestSceneUsage;
public override Dictionary<GUID, Hash128> DependencyHash => TestDependencyHash;
}
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";
static CalculateSceneDependencyData CreateDefaultBuildTask(List<GUID> scenes, BuildCache optionalCache, bool nonRecursive = false)
{
var task = new CalculateSceneDependencyData();
var testParams = new TestParams();
testParams.UseCache = optionalCache != null;
testParams.NonRecursiveDependencies = nonRecursive;
var testContent = new TestContent { TestScenes = scenes };
var testData = new TestDependencyData();
IBuildContext context = new BuildContext(testParams, testContent, testData, optionalCache);
ContextInjector.Inject(context, task);
return task;
}
static void ExtractTestData(IBuildTask task, out TestDependencyData dependencyData)
{
IBuildContext context = new BuildContext();
ContextInjector.Extract(context, task);
dependencyData = (TestDependencyData)context.GetContextObject<IDependencyData>();
}
static ObjectIdentifier MakeObjectId(string guid, long localIdentifierInFile, FileType fileType, string filePath)
{
var objectId = new ObjectIdentifier();
var boxed = (object)objectId;
var type = typeof(ObjectIdentifier);
type.GetField("m_GUID", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, new GUID(guid));
type.GetField("m_LocalIdentifierInFile", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, localIdentifierInFile);
type.GetField("m_FileType", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, fileType);
type.GetField("m_FilePath", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, filePath);
return (ObjectIdentifier)boxed;
}
[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);
}
[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);
}
static void SetupSceneForTest(out Scene scene, out GameObject prefab)
{
scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
prefab = AssetDatabase.LoadAssetAtPath<GameObject>(k_CubePath);
prefab.transform.position = new Vector3(0, 0, 0);
EditorUtility.SetDirty(prefab);
AssetDatabase.SaveAssets();
PrefabUtility.InstantiatePrefab(prefab);
EditorSceneManager.SaveScene(scene, k_ScenePath);
}
static object[] DependencyHashTestCases =
{
new object[] { true, (Action<object, object>)Assert.AreNotEqual },
new object[] { false, (Action<object, object>)Assert.AreEqual },
};
[TestCaseSource("DependencyHashTestCases")]
[Test]
public void CalculateSceneDependencyData_DependencyHashTests(bool modifyPrefab, Action<object, object> assertType)
{
SetupSceneForTest(out Scene scene, out GameObject prefab);
List<GUID> scenes = new List<GUID>();
GUID sceneGuid = new GUID(AssetDatabase.AssetPathToGUID(scene.path));
scenes.Add(sceneGuid);
TestDependencyData dependencyData1;
using (BuildCache cache = new BuildCache())
{
var buildTask = CreateDefaultBuildTask(scenes, cache);
buildTask.Run();
ExtractTestData(buildTask, out dependencyData1);
}
BuildCache.PurgeCache(false);
if (modifyPrefab)
prefab.transform.position = new Vector3(1, 1, 1);
EditorUtility.SetDirty(prefab);
AssetDatabase.SaveAssets();
TestDependencyData dependencyData2;
using (BuildCache cache = new BuildCache())
{
var buildTask = CreateDefaultBuildTask(scenes, cache);
buildTask.Run();
ExtractTestData(buildTask, out dependencyData2);
}
BuildCache.PurgeCache(false);
assertType(dependencyData1.DependencyHash[sceneGuid], dependencyData2.DependencyHash[sceneGuid]);
}
[Test]
public void CalculateSceneDependencyData_DependencyHash_IsZeroWhenNotUsingCashing()
{
SetupSceneForTest(out Scene scene, out var _);
List<GUID> scenes = new List<GUID>();
GUID sceneGuid = new GUID(AssetDatabase.AssetPathToGUID(scene.path));
scenes.Add(sceneGuid);
TestDependencyData dependencyData;
var buildTask = CreateDefaultBuildTask(scenes, null);
buildTask.Run();
ExtractTestData(buildTask, out dependencyData);
Assert.AreEqual(new Hash128(), dependencyData.DependencyHash[sceneGuid]);
}
[Test]
public void CalcualteSceneDependencyData_ReturnsNonEmptyUsage_ForNonRecursiveDependencies()
{
SetupSceneForTest(out Scene scene, out var _);
List<GUID> scenes = new List<GUID>();
GUID sceneGuid = new GUID(AssetDatabase.AssetPathToGUID(scene.path));
scenes.Add(sceneGuid);
TestDependencyData dependencyData;
var buildTask = CreateDefaultBuildTask(scenes, null, true);
buildTask.Run();
ExtractTestData(buildTask, out dependencyData);
BuildUsageTagSet usagetSet = dependencyData.SceneUsage[sceneGuid];
var method = typeof(BuildUsageTagSet).GetMethod("SerializeToJson", BindingFlags.Instance | BindingFlags.NonPublic);
var json = method.Invoke(usagetSet, new object[0]) as string;
Assert.AreNotEqual(json, "{\"m_objToUsage\":[]}");
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6c85b3ea86fd7d45a0284311e03aa02
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,161 @@
using System;
using NUnit.Framework;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Injector;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
class ContextInjectionTests
{
interface IInjectionContext : IContextObject
{
int State { get; set; }
}
class InjectionClass : IInjectionContext
{
public int State { get; set; }
}
struct InjectionStruct : IInjectionContext
{
public int State { get; set; }
}
struct TaskStruct : IBuildTask
{
public int Version { get { return 1; } }
public int NewState { get; private set; }
#pragma warning disable 649
[InjectContext]
internal IInjectionContext InjectedObject;
#pragma warning restore 649
public TaskStruct(int newState)
: this()
{
NewState = newState;
}
public ReturnCode Run()
{
InjectedObject.State = NewState;
return ReturnCode.Success;
}
}
class TaskClass : IBuildTask
{
public int Version { get { return 1; } }
public int NewState { get; private set; }
#pragma warning disable 649
[InjectContext]
internal IInjectionContext InjectedObject;
#pragma warning restore 649
public TaskClass(int newState)
{
NewState = newState;
}
public ReturnCode Run()
{
InjectedObject.State = NewState;
return ReturnCode.Success;
}
}
class TaskContext : IBuildTask
{
public int Version { get { return 1; } }
#pragma warning disable 649
[InjectContext]
internal IBuildContext InjectedContext;
#pragma warning restore 649
public ReturnCode Run()
{
return ReturnCode.Success;
}
}
[Test]
public void CanInjectAndExtractWithStructs()
{
IInjectionContext injection = new InjectionStruct();
injection.State = 1;
IBuildContext context = new BuildContext();
context.SetContextObject(injection);
TaskStruct task = new TaskStruct(2);
Assert.IsNull(task.InjectedObject);
// Still need to box / unbox the struct task
IBuildTask boxed = task;
ContextInjector.Inject(context, boxed);
task = (TaskStruct)boxed;
Assert.IsNotNull(task.InjectedObject);
Assert.AreEqual(1, task.InjectedObject.State);
ReturnCode result = task.Run();
Assert.AreEqual(ReturnCode.Success, result);
ContextInjector.Extract(context, task);
IInjectionContext modifiedInjection = context.GetContextObject<IInjectionContext>();
Assert.AreEqual(task.NewState, modifiedInjection.State);
}
[Test]
public void CanInjectAndExtractWithClasses()
{
IInjectionContext injection = new InjectionClass();
injection.State = 1;
IBuildContext context = new BuildContext();
context.SetContextObject(injection);
TaskClass task = new TaskClass(2);
Assert.IsNull(task.InjectedObject);
ContextInjector.Inject(context, task);
Assert.IsNotNull(task.InjectedObject);
Assert.AreEqual(1, task.InjectedObject.State);
ReturnCode result = task.Run();
Assert.AreEqual(ReturnCode.Success, result);
ContextInjector.Extract(context, task);
IInjectionContext modifiedInjection = context.GetContextObject<IInjectionContext>();
Assert.AreEqual(task.NewState, modifiedInjection.State);
}
[Test]
public void CanInjectIBuildContextAsInOnly()
{
IBuildContext context = new BuildContext();
TaskContext task = new TaskContext();
Assert.IsNull(task.InjectedContext);
ContextInjector.Inject(context, task);
Assert.IsNotNull(task.InjectedContext);
Assert.AreEqual(context, task.InjectedContext);
Assert.Throws<InvalidOperationException>(() =>
{
ContextInjector.Extract(context, task);
});
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 288dd00735888d6419c1d6312c8a1d51
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,61 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using NUnit.Framework;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Pipeline.Utilities;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
public class ContextObjectSerializationTests
{
public Type[] GetIContextObjectTypes()
{
var blacklist = new[]
{
typeof(BuildCallbacks), typeof(Unity5PackedIdentifiers), typeof(PrefabPackedIdentifiers), typeof(LinearPackedIdentifiers), typeof(BuildCache),
typeof(ProgressTracker), typeof(ProgressLoggingTracker), typeof(BuildInterfacesWrapper)
#if UNITY_2022_2_OR_NEWER
, typeof(ContentFileIdentifiers)
, typeof(ClusterOutput)
#endif
};
var assembly = AppDomain.CurrentDomain.GetAssemblies().First(x => x.GetName().Name == "Unity.ScriptableBuildPipeline.Editor");
return assembly.GetTypes().Where(x => typeof(IContextObject).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract).Where(x => !blacklist.Contains(x)).ToArray();
}
[Test]
public void IContextObjects_SupportSerialization()
{
// This is just a generic catch all to ensure we properly setup C# serialization on IContextTypes
// More explicit tests should be written per type to validate proper serialization in / out
var types = GetIContextObjectTypes();
foreach (var type in types)
IContextObject_SupportSerialization(type);
}
static T SerializedAndDeserializeObject<T>(T obj)
{
var formatter = new BinaryFormatter();
using (var stream = new MemoryStream())
{
formatter.Serialize(stream, obj);
stream.Position = 0;
var obj2 = (T)formatter.Deserialize(stream);
return obj2;
}
}
static void IContextObject_SupportSerialization(Type type)
{
var instance1 = (IContextObject)Activator.CreateInstance(type, true);
var instance2 = SerializedAndDeserializeObject(instance1);
Assert.NotNull(instance2);
Assert.AreEqual(type, instance2.GetType());
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa12bf74f16f5e84f93111cdd55bf70b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,80 @@
using System.IO;
using NUnit.Framework;
using UnityEditor.Build.Pipeline.Utilities;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
class FileCompressorTests
{
const string k_SourceDirectory = "Compressor";
static readonly string[] k_SourceFiles =
{
"/File1.json",
"/Subdir/File2.json",
"\\File3.json",
"\\Subdir\\File4.json"
};
static string NormalizePath(string path)
{
return path.Replace("\\", "/");
}
[OneTimeSetUp]
public void OneTimeSetUp()
{
foreach (string file in k_SourceFiles)
{
var filePath = NormalizePath(k_SourceDirectory + file);
var dir = Path.GetDirectoryName(filePath);
Directory.CreateDirectory(dir);
File.WriteAllText(filePath, filePath);
}
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
Directory.Delete(k_SourceDirectory, true);
}
[Test]
public void CompressAndDecompressCanHandleSubdirectories()
{
var targetDirectory = k_SourceDirectory + "2";
var success = FileCompressor.Compress(k_SourceDirectory, "artifacts.sbpGz");
Assert.IsTrue(success);
success = FileCompressor.Decompress("artifacts.sbpGz", targetDirectory);
Assert.IsTrue(success);
for (int i = 0; i < k_SourceFiles.Length; i++)
{
var sourcePath = NormalizePath(k_SourceDirectory + k_SourceFiles[i]);
var targetPath = NormalizePath(targetDirectory + k_SourceFiles[i]);
FileAssert.Exists(targetPath);
FileAssert.AreEqual(sourcePath, targetPath);
}
File.Delete("artifacts.sbpGz");
Directory.Delete(targetDirectory, true);
}
[Test]
public void TrailingSlashDoesNotChangeResults()
{
var success = FileCompressor.Compress(k_SourceDirectory, "artifacts1.sbpGz");
Assert.IsTrue(success);
success = FileCompressor.Compress(k_SourceDirectory + "/", "artifacts2.sbpGz");
Assert.IsTrue(success);
FileAssert.AreEqual("artifacts1.sbpGz", "artifacts2.sbpGz");
File.Delete("artifacts1.sbpGz");
File.Delete("artifacts2.sbpGz");
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d2f1f46c2a196649b1eb9cd319cc991
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Utilities;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
class GenerateBundlePackingTests
{
IDependencyData GetDependencyData(List<ObjectIdentifier> objects, params GUID[] guids)
{
IDependencyData dep = new BuildDependencyData();
for (int i = 0; i < guids.Length; i++)
{
AssetLoadInfo loadInfo = new AssetLoadInfo()
{
asset = guids[i],
address = $"path{i}",
includedObjects = objects,
referencedObjects = objects
};
dep.AssetInfo.Add(guids[i], loadInfo);
}
return dep;
}
List<ObjectIdentifier> CreateObjectIdentifierList(string path, params GUID[] guids)
{
var objects = new List<ObjectIdentifier>();
foreach (GUID guid in guids)
{
var obj = new ObjectIdentifier();
obj.SetObjectIdentifier(guid, 0, FileType.SerializedAssetType, path);
objects.Add(obj);
}
return objects;
}
string k_TempAsset = "Assets/temp.prefab";
GUID k_TempGuid;
[OneTimeSetUp]
public void OneTimeSetUp()
{
var root = GameObject.CreatePrimitive(PrimitiveType.Cube);
var child = GameObject.CreatePrimitive(PrimitiveType.Cube);
child.transform.parent = root.transform;
PrefabUtility.SaveAsPrefabAsset(root, k_TempAsset);
k_TempGuid = new GUID(AssetDatabase.AssetPathToGUID(k_TempAsset));
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
AssetDatabase.DeleteAsset(k_TempAsset);
}
[Test]
public void WhenReferencesAreUnique_FilterReferencesForAsset_ReturnsReferences()
{
var assetInBundle = new GUID("00000000000000000000000000000000");
List<ObjectIdentifier> objects = CreateObjectIdentifierList("path", assetInBundle, assetInBundle);
IDependencyData dep = GetDependencyData(objects, assetInBundle);
var references = new List<ObjectIdentifier>(objects);
List<GUID> results = GenerateBundlePacking.FilterReferencesForAsset(dep, assetInBundle, references);
Assert.AreEqual(1, results.Count);
Assert.AreEqual(assetInBundle, results[0]);
}
[Test]
public void WhenReferencesContainsDefaultResources_FilterReferencesForAsset_PrunesDefaultResources()
{
var assetInBundle = new GUID("00000000000000000000000000000000");
List<ObjectIdentifier> objects = CreateObjectIdentifierList(CommonStrings.UnityDefaultResourcePath, assetInBundle);
IDependencyData dep = GetDependencyData(objects, assetInBundle);
var references = new List<ObjectIdentifier>(objects);
GenerateBundlePacking.FilterReferencesForAsset(dep, assetInBundle, references);
Assert.AreEqual(0, references.Count);
}
[Test]
public void WhenReferencesContainsAssetsInBundles_FilterReferencesForAsset_PrunesAssetsInBundles()
{
var assetInBundle = new GUID("00000000000000000000000000000000");
List<ObjectIdentifier> objects = CreateObjectIdentifierList("path", assetInBundle);
IDependencyData dep = GetDependencyData(objects, assetInBundle);
var references = new List<ObjectIdentifier>(objects);
GenerateBundlePacking.FilterReferencesForAsset(dep, assetInBundle, references);
Assert.AreEqual(0, references.Count);
}
[Test]
public void WhenReferencesDoesNotContainAssetsInBundles_FilterReferences_PrunesNothingAndReturnsNothing()
{
var assetInBundle = new GUID("00000000000000000000000000000000");
List<ObjectIdentifier> objects = CreateObjectIdentifierList("path", assetInBundle);
IDependencyData dep = new BuildDependencyData();
var references = new List<ObjectIdentifier>(objects);
List<GUID> results = GenerateBundlePacking.FilterReferencesForAsset(dep, assetInBundle, references);
Assert.AreEqual(1, references.Count);
Assert.AreEqual(assetInBundle, references[0].guid);
Assert.AreEqual(0, results.Count);
}
[Test]
public void WhenReferencesContainsRefsIncludedByNonCircularAssets_FilterReferencesForAsset_PrunesRefsIncludedByNonCircularAssets()
{
var assetNotInBundle = new GUID("00000000000000000000000000000000");
var referenceInBundle = new GUID("00000000000000000000000000000001");
var referenceNotInBundle = new GUID("00000000000000000000000000000002");
List<ObjectIdentifier> objects = CreateObjectIdentifierList("path", referenceNotInBundle);
IDependencyData dep = GetDependencyData(objects, referenceInBundle);
List<ObjectIdentifier> references = CreateObjectIdentifierList("path", referenceInBundle, referenceNotInBundle);
GenerateBundlePacking.FilterReferencesForAsset(dep, assetNotInBundle, references);
Assert.AreEqual(0, references.Count);
}
[Test]
public void WhenReferencesContainsRefsIncludedByCircularAssetsWithLowerGuid_FilterReferencesForAsset_PrunesRefsIncludedByCircularAssetsWithLowerGuid()
{
var assetNotInBundle = new GUID("00000000000000000000000000000001");
var referenceInBundle = new GUID("00000000000000000000000000000000");
List<ObjectIdentifier> objects = CreateObjectIdentifierList("path", assetNotInBundle); // circular reference to asset whose references we want to filter
IDependencyData dep = GetDependencyData(objects, referenceInBundle);
List<ObjectIdentifier> references = CreateObjectIdentifierList("path", referenceInBundle, assetNotInBundle);
GenerateBundlePacking.FilterReferencesForAsset(dep, assetNotInBundle, references);
Assert.AreEqual(0, references.Count);
}
[Test]
public void WhenReferencesContainsRefsIncludedByCircularAssetsWithHigherGuid_FilterReferencesForAsset_DoesNotPruneRefsIncludedByCircularAssetsWithHigherGuid()
{
var assetNotInBundle = new GUID("00000000000000000000000000000000");
var referenceInBundle = new GUID("00000000000000000000000000000001");
List<ObjectIdentifier> objects = CreateObjectIdentifierList("path", assetNotInBundle); // circular reference to asset whose references we want to filter
IDependencyData dep = GetDependencyData(objects, referenceInBundle);
List<ObjectIdentifier> references = CreateObjectIdentifierList("path", referenceInBundle, assetNotInBundle);
GenerateBundlePacking.FilterReferencesForAsset(dep, assetNotInBundle, references);
Assert.AreEqual(1, references.Count);
Assert.AreEqual(assetNotInBundle, references[0].guid);
}
[Test]
public void WhenReferencesContainsPreviousSceneObjects_FilterReferencesForAsset_PrunesPreviousSceneObjects()
{
var assetInBundle = new GUID("00000000000000000000000000000001");
var referenceNotInBundle = new GUID("00000000000000000000000000000000");
List<ObjectIdentifier> objects = CreateObjectIdentifierList("path", referenceNotInBundle);
IDependencyData dep = GetDependencyData(objects, assetInBundle);
var references = new List<ObjectIdentifier>(objects);
GenerateBundlePacking.FilterReferencesForAsset(dep, assetInBundle, references, new HashSet<ObjectIdentifier>() { objects[0] });
Assert.AreEqual(0, references.Count);
}
[Test]
public void WhenReferencesContainPreviousSceneAssetDependencies_FilterReferencesForAsset_PrunesPreviousAssetDependencies([Values] bool containsPreviousSceneAsset)
{
var assetInBundle = new GUID("00000000000000000000000000000001");
var referenceNotInBundle = new GUID("00000000000000000000000000000000");
List<ObjectIdentifier> objects = CreateObjectIdentifierList("path", referenceNotInBundle);
IDependencyData dep = GetDependencyData(objects, assetInBundle);
var references = new List<ObjectIdentifier>(objects);
var previousSceneReferences = new HashSet<GUID>();
if (containsPreviousSceneAsset)
previousSceneReferences.Add(assetInBundle);
GenerateBundlePacking.FilterReferencesForAsset(dep, assetInBundle, references, new HashSet<ObjectIdentifier>(), previousSceneReferences);
if (containsPreviousSceneAsset)
Assert.AreEqual(0, references.Count);
else
Assert.AreEqual(1, references.Count);
}
#if !UNITY_2019_1_OR_NEWER
[Test]
public void WhenPrefabContainsDuplicateTypes_GetSortedSceneObjectIdentifiers_DoesNotThorwError()
{
var includes = new List<ObjectIdentifier>(ContentBuildInterface.GetPlayerObjectIdentifiersInAsset(k_TempGuid, EditorUserBuildSettings.activeBuildTarget));
var sorted = GenerateBundleCommands.GetSortedSceneObjectIdentifiers(includes);
Assert.AreEqual(includes.Count, sorted.Count);
}
#endif
[Test]
public void BuildCacheUtility_GetSortedUniqueTypesForObjects_ReturnsUniqueAndSortedTypeArray()
{
var includes = ContentBuildInterface.GetPlayerObjectIdentifiersInAsset(k_TempGuid, EditorUserBuildSettings.activeBuildTarget);
// Test prefab is created using 2 primitive cubes, one parented to the other, so the includes will in turn contain the sequence 2x:
Type[] expectedTypes = new[] { typeof(GameObject), typeof(Transform), typeof(MeshFilter), typeof(MeshRenderer), typeof(BoxCollider) };
Array.Sort(expectedTypes, (x, y) => x.AssemblyQualifiedName.CompareTo(y.AssemblyQualifiedName));
var actualTypes = BuildCacheUtility.GetSortedUniqueTypesForObjects(includes);
Assert.AreEqual(expectedTypes.Length * 2, includes.Length);
Assert.AreEqual(expectedTypes.Length, actualTypes.Length);
CollectionAssert.AreEqual(expectedTypes, actualTypes);
}
[Test]
public void BuildCacheUtility_GetMainTypeForObjects_ReturnsUniqueAndSortedTypeArray()
{
var includes = ContentBuildInterface.GetPlayerObjectIdentifiersInAsset(k_TempGuid, EditorUserBuildSettings.activeBuildTarget);
// Test prefab is created using 2 primitive cubes, one parented to the other, so the includes will in turn contain the sequence:
Type[] expectedTypes = new[] { typeof(GameObject), typeof(Transform), typeof(MeshFilter), typeof(MeshRenderer), typeof(BoxCollider),
typeof(GameObject), typeof(Transform), typeof(MeshFilter), typeof(MeshRenderer), typeof(BoxCollider) };
// One catch, the ordering of the expected types is based on the order of includes which is in turn ordered by the local identifier in file.
// Since we are generating the prefab as part of the test, and lfids generation is random, we don't know what order they will be returned in.
// So sort both expected types lists and compare exact.
Array.Sort(expectedTypes, (x, y) => x.AssemblyQualifiedName.CompareTo(y.AssemblyQualifiedName));
var actualTypes = BuildCacheUtility.GetMainTypeForObjects(includes);
Array.Sort(actualTypes, (x, y) => x.AssemblyQualifiedName.CompareTo(y.AssemblyQualifiedName));
Assert.AreEqual(expectedTypes.Length, includes.Length);
Assert.AreEqual(expectedTypes.Length, actualTypes.Length);
CollectionAssert.AreEqual(expectedTypes, actualTypes);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a896103a5441cc040ab6d184ad038c6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,168 @@
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
class HashingHelpersTests
{
class AssertExpectedLayout : Attribute
{
public Type CompareType;
public AssertExpectedLayout(Type compareType)
{
CompareType = compareType;
}
}
[AssertExpectedLayout(typeof(PreloadInfo))]
class ExpectedPreloadInfo
{
public List<ObjectIdentifier> preloadObjects { get; set; }
}
[AssertExpectedLayout(typeof(AssetBundleInfo))]
class ExpectedAssetBundleInfo
{
public string bundleName { get; set; }
public List<AssetLoadInfo> bundleAssets { get; set; }
}
[AssertExpectedLayout(typeof(WriteCommand))]
class ExpectedWriteCommand
{
public string fileName { get; set; }
public string internalName { get; set; }
public List<SerializationInfo> serializeObjects { get; set; }
}
[AssertExpectedLayout(typeof(SerializationInfo))]
class ExpectedSerializationInfo
{
public ObjectIdentifier serializationObject { get; set; }
public long serializationIndex { get; set; }
}
[AssertExpectedLayout(typeof(ObjectIdentifier))]
class ExpectedObjectIdentifier
{
public GUID guid { get; }
public long localIdentifierInFile { get; }
public FileType fileType { get; }
public string filePath { get; }
}
public static IEnumerable CustomHashTypes
{
get
{
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
foreach (AssertExpectedLayout attr in type.GetCustomAttributes<AssertExpectedLayout>(true))
{
yield return new TestCaseData(type, attr.CompareType).SetName($"CompareLayout_{type.Name}_{attr.CompareType.Name}");
}
}
}
// README! If this test fails, update the associated hashing function in HashingHelpers.cs and then
// update the expected type above to reflect the new layout.
// The purpose of this test is to make sure our custom hashing function is updated if the associate engine type changes.
[Test, TestCaseSource(typeof(HashingHelpersTests), "CustomHashTypes")]
public static void CompareLayout(Type expected, Type actual)
{
{
PropertyInfo[] eInfo = expected.GetProperties();
PropertyInfo[] aInfo = actual.GetProperties();
Assert.AreEqual(eInfo.Length, aInfo.Length);
for (int i = 0; i < eInfo.Length; i++)
Assert.AreEqual(eInfo[i].PropertyType, aInfo[i].PropertyType);
}
{
FieldInfo[] eInfo = expected.GetFields();
FieldInfo[] aInfo = actual.GetFields();
Assert.AreEqual(eInfo.Length, aInfo.Length);
for (int i = 0; i < eInfo.Length; i++)
Assert.AreEqual(eInfo[i].FieldType, aInfo[i].FieldType);
}
}
[Test]
public static void PreloadInfo_WhenValueChanges_HashesChange()
{
ObjectIdentifier obj1 = new ObjectIdentifier();
obj1.SetFilePath("TestPath");
PreloadInfo[] infos = new PreloadInfo[]
{
new PreloadInfo() { preloadObjects = new List<ObjectIdentifier>() },
new PreloadInfo() { preloadObjects = new List<ObjectIdentifier>() { new ObjectIdentifier() } },
new PreloadInfo() { preloadObjects = new List<ObjectIdentifier>() { obj1 } },
};
HashSet<Hash128> set = new HashSet<Hash128>(infos.Select(x => HashingHelpers.GetHash128(x)));
Assert.AreEqual(infos.Length, set.Count);
}
[Test]
public static void SerializationInfo_WhenValueChanges_HashesChange()
{
ObjectIdentifier obj1 = new ObjectIdentifier();
obj1.SetFilePath("TestPath");
SerializationInfo[] infos = new SerializationInfo[]
{
new SerializationInfo() { serializationIndex = 0 },
new SerializationInfo() { serializationIndex = 1 },
new SerializationInfo() { serializationIndex = 0, serializationObject = obj1 }
};
HashSet<Hash128> set = new HashSet<Hash128>(infos.Select(x => HashingHelpers.GetHash128(x)));
Assert.AreEqual(infos.Length, set.Count);
}
[Test]
public static void AssetBundleInfo_WhenValueChanges_HashesChange()
{
AssetBundleInfo[] infos = new AssetBundleInfo[]
{
new AssetBundleInfo() { bundleName = "Test" },
new AssetBundleInfo() { bundleName = "Test2" },
new AssetBundleInfo() { bundleAssets = new List<AssetLoadInfo>() { new AssetLoadInfo() { address = "a1" } } },
new AssetBundleInfo() { bundleAssets = new List<AssetLoadInfo>() { new AssetLoadInfo() { address = "a2" } } }
};
HashSet<Hash128> set = new HashSet<Hash128>(infos.Select(x => HashingHelpers.GetHash128(x)));
Assert.AreEqual(infos.Length, set.Count);
}
[Test]
public static void AssetLoadInfo_WhenValueChanges_HashesChange()
{
AssetLoadInfo[] infos = new AssetLoadInfo[]
{
new AssetLoadInfo() { address = "Test" },
new AssetLoadInfo() { asset = GUID.Generate()},
new AssetLoadInfo() { includedObjects = new List<ObjectIdentifier>(){ new ObjectIdentifier()} },
new AssetLoadInfo() { referencedObjects = new List<ObjectIdentifier>() { new ObjectIdentifier() } }
};
HashSet<Hash128> set = new HashSet<Hash128>(infos.Select(x => HashingHelpers.GetHash128(x)));
Assert.AreEqual(infos.Length, set.Count);
}
[Test]
public static void WriteCommand_WhenValueChanges_HashesChange()
{
WriteCommand[] infos = new WriteCommand[]
{
new WriteCommand() { fileName = "Test" },
new WriteCommand() { internalName = "Test2" },
new WriteCommand() { serializeObjects = new List<SerializationInfo>() { new SerializationInfo() }},
new WriteCommand() { serializeObjects = new List<SerializationInfo>() { new SerializationInfo() { serializationIndex = 2 } }}
};
HashSet<Hash128> set = new HashSet<Hash128>(infos.Select(x => HashingHelpers.GetHash128(x)));
Assert.AreEqual(infos.Length, set.Count);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93cc378d60f13644e9669c22e5cb4618
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,295 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
class HashingMethodsTests
{
[Test]
public void CsMD4_Calcualte_FileNames_IdenticalTo_CppMD4()
{
// Test ensures our C# implementation of MD4 matches our expected C++ MD4 for compatibility
// Until which time we no longer care about compatibility with BuildPipeline.BuildAssetBundles
var sourceNames = new[]
{
"basic_sprite",
"audio",
"prefabs",
"shaderwithcollection",
"multi_sprite_packed"
};
var expectedNames = new[]
{
"a67a7313ceb7840411094318a4aa7055",
"d5ae2b5aa3edc0f73b4bb6b1ae125a53",
"6fee5e41c4939eed80f81beb3e5e6ebc",
"dc5d7d3a7d9efcf91a0314cdcc3af3c8",
"ddc8dcea83a5ff418d94c6a1623e81ad"
};
for (var i = 0; i < 5; i++)
Assert.AreEqual(expectedNames[i], HashingMethods.Calculate<MD4>(sourceNames[i]).ToString());
}
[Test]
public void CsMD4_Calcualte_FileIDs_IdenticalTo_CppMD4()
{
// Test ensures our C# implementation of MD4 matches our expected C++ MD4 for compatibility
// Until which time we no longer care about compatibility with BuildPipeline.BuildAssetBundles
Assert.AreEqual(-7588530676450950513, BitConverter.ToInt64(HashingMethods.Calculate<MD4>("fb3a9882e5510684697de78116693750", FileType.MetaAssetType, (long)21300000).ToBytes(), 0));
Assert.AreEqual(-8666180608703991793, BitConverter.ToInt64(HashingMethods.Calculate<MD4>("library/atlascache/27/2799803afb660251e3b3049ba37cb15a", (long)2).ToBytes(), 0));
}
#if UNITY_2020_1_OR_NEWER
[TestCase(false)] // Use old hasher (MD5)
[TestCase(true)] // Use V2 hasher (Spooky)
public void HashingMethods_Has128x2Fast_SameAsGeneric(bool useV2Hasher)
{
// Test ensures the HashingMethods.Calculate(Hash128, Hash128) fast path produces the same results as the general HashingMethods.Calculate(params object[] objects) one does
Hash128 hash1 = new Hash128(0x1122334455667788, 0x99AABBCCDDEEFF00);
Hash128 hash2 = new Hash128(0x123456789ABCDEF0, 0x1967AbC487Df2F12);
bool prevUseV2Hasher = ScriptableBuildPipeline.useV2Hasher;
ScriptableBuildPipeline.useV2Hasher = useV2Hasher;
Assert.AreEqual(HashingMethods.Calculate(hash1, hash2), HashingMethods.Calculate(new object[] { hash1, hash2 }));
ScriptableBuildPipeline.useV2Hasher = prevUseV2Hasher;
}
#else
[Test]
public void HashingMethods_Has128x2Fast_SameAsGeneric()
{
// Test ensures the HashingMethods.Calculate(Hash128, Hash128) fast path produces the same results as the general HashingMethods.Calculate(params object[] objects) one does
Hash128 hash1 = new Hash128(0x1122334455667788, 0x99AABBCCDDEEFF00);
Hash128 hash2 = new Hash128(0x123456789ABCDEF0, 0x1967AbC487Df2F12);
Assert.AreEqual(HashingMethods.Calculate(hash1, hash2), HashingMethods.Calculate(new object[] { hash1, hash2 }));
}
#endif
// Struct that is binary compatible with Hash128 but is not Hash128
struct Hash128Proxy
{
public uint m_Value0;
public uint m_Value1;
public uint m_Value2;
public uint m_Value3;
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_Hash128RawBytes_SameAsGeneric(IHasher hashFunc)
{
// Test ensures the HashingMethods.GetRawBytes() function's fast-path for Hash128 type produces the same results as generic but slower reflection based path it replaces
Hash128 hash128 = new Hash128(0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00);
Hash128Proxy hash128Proxy = new Hash128Proxy() { m_Value0 = 0x11223344, m_Value1 = 0x55667788, m_Value2 = 0x99AABBCC, m_Value3 = 0xDDEEFF00 };
Assert.AreEqual(hashFunc.Calculate(hash128), hashFunc.Calculate(hash128Proxy));
}
// Struct that is binary compatible with GUID but is not GUID
struct GUIDProxy
{
public uint m_Value0;
public uint m_Value1;
public uint m_Value2;
public uint m_Value3;
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_GuidRawBytes_SameAsGeneric(IHasher hashFunc)
{
// Test ensures the HashingMethods.GetRawBytes() function's fast-path for GUID type produces the same results as generic but slower reflection based path it replaces
GUID guid = new GUID("4433221188776655CCBBAA9900FFEEDD");
GUIDProxy guidProxy = new GUIDProxy() { m_Value0 = 0x11223344, m_Value1 = 0x55667788, m_Value2 = 0x99AABBCC, m_Value3 = 0xDDEEFF00 };
Assert.AreEqual(hashFunc.Calculate(guid), hashFunc.Calculate(guidProxy));
}
public interface IHasher
{
Type HashType();
RawHash Calculate(object obj);
RawHash Calculate(params object[] objects);
RawHash CalculateStream(Stream stream);
}
public class HashTester<T> : IHasher where T : HashAlgorithm
{
public Type HashType() => typeof(T);
public RawHash Calculate(object obj) => HashingMethods.Calculate<T>(obj);
public RawHash Calculate(params object[] objects) => HashingMethods.Calculate<T>(objects);
public RawHash CalculateStream(Stream stream) => HashingMethods.CalculateStream<T>(stream);
public override string ToString() => $"{typeof(T).Name}";
}
public enum TestIndex
{
Array,
List,
HashSet,
Dictionary,
Offset,
Unicode,
Identical
}
public static Dictionary<Type, string[]> TestResults = new Dictionary<Type, string[]>
{
// TestResults format:
// { Hashing Type,
// new[] { "Array Hash ", "List Hash ", "HashSet Hash",
// "Dictionary Hash", "Offset Hash", "Unicode Hash",
// "Identical Hash " } }
{ typeof(MD4),
new[] { "99944412d5093e431ba7ccdaf48f44f3", "99944412d5093e431ba7ccdaf48f44f3", "99944412d5093e431ba7ccdaf48f44f3",
"34392e04ec079d34cd861df956db2099", "086143d6671971fcdd40d96af36c0e92", "452f83421f98bf3831b7fd4217af3f92",
"65de0f26e6502fa975d8ea0b79806517" } },
{ typeof(MD5),
new[] { "6d489a02294c1a5ce775050cfa2cd363", "6d489a02294c1a5ce775050cfa2cd363", "6d489a02294c1a5ce775050cfa2cd363",
"2844dc4c3aa734b2cae0f4a670d5346e", "384a563d6a14deb7553f7efc95d1b67f", "c434697711429f8205b09c47bcd87d85",
"5e5335125abe521354a9f9a7c302d690" } }
#if UNITY_2020_1_OR_NEWER
, { typeof(SpookyHash),
new[] { "6e59a12bc07db93b5f9e6a0a4acecbd1", "6e59a12bc07db93b5f9e6a0a4acecbd1", "6e59a12bc07db93b5f9e6a0a4acecbd1",
"bca5ae54cb78244bd544d06111694efb", "0fa373cc7984b0e05e7052fd7a7e51eb", "2e6baf5f29327f5ea5d668c988307232",
"5d212d4d612906870257cb183932d1a7" } }
#endif
};
public static IEnumerable<IHasher> TestCases()
{
yield return new HashTester<MD4>();
yield return new HashTester<MD5>();
#if UNITY_2020_1_OR_NEWER
yield return new HashTester<SpookyHash>();
#endif
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_ProducesValidHashFor_Array(IHasher hashFunc)
{
var sourceNames = new[]
{
"basic_sprite",
"audio",
"prefabs",
"shaderwithcollection",
"multi_sprite_packed"
};
// Use (object) cast so Calculate doesn't expand the array and use params object[] objects case
Assert.AreEqual(TestResults[hashFunc.HashType()][(int)TestIndex.Array], hashFunc.Calculate((object)sourceNames).ToString());
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_ProducesValidHashFor_List(IHasher hashFunc)
{
var sourceNames = new List<string>
{
"basic_sprite",
"audio",
"prefabs",
"shaderwithcollection",
"multi_sprite_packed"
};
Assert.AreEqual(TestResults[hashFunc.HashType()][(int)TestIndex.List], hashFunc.Calculate(sourceNames).ToString());
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_ProducesValidHashFor_HashSet(IHasher hashFunc)
{
var sourceNames = new HashSet<string>
{
"basic_sprite",
"audio",
"prefabs",
"shaderwithcollection",
"multi_sprite_packed"
};
Assert.AreEqual(TestResults[hashFunc.HashType()][(int)TestIndex.HashSet], hashFunc.Calculate(sourceNames).ToString());
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_ProducesValidHashFor_Dictionary(IHasher hashFunc)
{
var sourceNames = new Dictionary<string, string>
{
{ "basic_sprite", "audio" },
{ "prefabs", "shaderwithcollection" }
};
Assert.AreEqual(TestResults[hashFunc.HashType()][(int)TestIndex.Dictionary], hashFunc.Calculate(sourceNames).ToString());
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_ProducesValidHashFor_OffsetStreams(IHasher hashFunc)
{
byte[] bytes = { 0xe1, 0x43, 0x2f, 0x83, 0xdf, 0xeb, 0xa8, 0x86, 0xfb, 0xfe, 0xc9, 0x97, 0x20, 0xfb, 0x53, 0x45,
0x24, 0x5d, 0x92, 0x8b, 0xa2, 0xc4, 0xe1, 0xe2, 0x48, 0x4a, 0xbb, 0x66, 0x43, 0x9a, 0xbc, 0x84 };
using (var stream = new MemoryStream(bytes))
{
stream.Position = 16;
RawHash hash1 = hashFunc.CalculateStream(stream);
stream.Position = 0;
RawHash hash2 = hashFunc.CalculateStream(stream);
Assert.AreNotEqual(hash1.ToString(), hash2.ToString());
Assert.AreEqual(TestResults[hashFunc.HashType()][(int)TestIndex.Offset], hash1.ToString());
}
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_ProducesValidHashFor_UnicodeStrings(IHasher hashFunc)
{
// It might not look it at first glance, but the below 2 strings are indeed different!
// The ASCII byte representation of both of these strings is identical, which was causing
// hashing methods to return identical hashes as we had used ASCII for everything.
string str1 = "[기본]양손무기";
string str2 = "[기본]한손무기";
RawHash hash1 = hashFunc.Calculate(str1);
RawHash hash2 = hashFunc.Calculate(str2);
Assert.AreNotEqual(str1, str2);
Assert.AreNotEqual(hash1, hash2);
Assert.AreEqual(TestResults[hashFunc.HashType()][(int)TestIndex.Unicode], hash1.ToString());
}
[Test]
[TestCaseSource("TestCases")]
public void HashingMethods_ProducesValidHashFor_IdenticalCalculateCalls(IHasher hashFunc)
{
// This test seems silly, but has exposed issues with HashingMethods not being deterministic internally when run back to back (SpookyHash)
var hash1 = hashFunc.Calculate("HashingMethods_ProducesValidHashFor_IdenticalCalculateCalls");
var hash2 = hashFunc.Calculate("HashingMethods_ProducesValidHashFor_IdenticalCalculateCalls");
Assert.IsTrue(hash1.Equals(hash2));
Assert.AreEqual(TestResults[hashFunc.HashType()][(int)TestIndex.Identical], hash1.ToString());
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d628170b66f4d154eb5dda6859439c32
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,201 @@
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor.Build.Pipeline.Utilities;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
public class LinkXMLGeneratorTests
{
const string k_LinkFile = "link.xml";
[TearDown]
public void OnTearDown()
{
if (File.Exists(k_LinkFile))
File.Delete(k_LinkFile);
}
public static string ReadLinkXML(string linkFile, out int assemblyCount, out int typeCount)
{
FileAssert.Exists(linkFile);
var fileText = File.ReadAllText(linkFile);
assemblyCount = Regex.Matches(fileText, "<assembly").Count;
typeCount = Regex.Matches(fileText, "<type").Count;
return fileText;
}
public static void AssertTypePreserved(string input, Type t)
{
StringAssert.IsMatch($"type.*?{t.FullName}.*?preserve=\"all\"", input);
}
public static void AssertTypeWithAttributePreserved(string input, string fullName)
{
StringAssert.IsMatch($"type.*?{fullName}.*? preserve=\"nothing\" serialized=\"true\"", input);
}
public static void AssertAssemblyPreserved(string input, Assembly a)
{
StringAssert.IsMatch($"assembly.*?{a.FullName}.*?preserve=\"all\"", input);
}
public static void AssertAssemblyFullName(string input, string assemblyName)
{
StringAssert.IsMatch($"assembly.*?{assemblyName}", input);
}
[Test]
public void CreateDefault_Converts_ExpectedUnityEditorTypes()
{
var types = LinkXmlGenerator.GetEditorTypeConversions();
var editorTypes = types.Select(x => x.Key).ToArray();
var runtimeTypes = types.Select(x => x.Value).ToArray();
var assemblies = runtimeTypes.Select(x => x.Assembly).Distinct().ToArray();
var link = LinkXmlGenerator.CreateDefault();
link.AddTypes(editorTypes);
link.Save(k_LinkFile);
var xml = ReadLinkXML(k_LinkFile, out int assemblyCount, out int typeCount);
Assert.AreEqual(assemblyCount, assemblies.Length);
Assert.AreEqual(typeCount, runtimeTypes.Length);
foreach (var t in runtimeTypes)
AssertTypePreserved(xml, t);
}
[Test]
public void CreateDefault_DoesNotConvert_UnexpectedUnityEditorTypes()
{
var unexpectedType = typeof(UnityEditor.BuildPipeline);
var link = LinkXmlGenerator.CreateDefault();
link.AddTypes(new[] { unexpectedType });
link.Save(k_LinkFile);
var xml = ReadLinkXML(k_LinkFile, out int assemblyCount, out int typeCount);
Assert.AreEqual(assemblyCount, 1);
Assert.AreEqual(typeCount, 1);
AssertTypePreserved(xml, unexpectedType);
}
[Test]
public void LinkXML_Preserves_MultipleTypes_FromMultipleAssemblies()
{
var types = new[] { typeof(UnityEngine.MonoBehaviour), typeof(UnityEngine.Build.Pipeline.CompatibilityAssetBundleManifest) };
var link = new LinkXmlGenerator();
link.AddTypes(types);
link.Save(k_LinkFile);
var xml = ReadLinkXML(k_LinkFile, out int assemblyCount, out int typeCount);
Assert.AreEqual(assemblyCount, 2);
Assert.AreEqual(typeCount, types.Length);
foreach (var t in types)
AssertTypePreserved(xml, t);
}
[Test]
public void LinkXML_Preserves_Assemblies()
{
var assemblies = new[] { typeof(UnityEngine.MonoBehaviour).Assembly, typeof(UnityEngine.Build.Pipeline.CompatibilityAssetBundleManifest).Assembly };
var link = new LinkXmlGenerator();
link.AddAssemblies(assemblies);
link.Save(k_LinkFile);
var xml = ReadLinkXML(k_LinkFile, out int assemblyCount, out int typeCount);
Assert.AreEqual(assemblyCount, assemblies.Length);
Assert.AreEqual(typeCount, 0);
foreach (var a in assemblies)
AssertAssemblyPreserved(xml, a);
}
[Test]
public void LinkXML_Preserves_SerializeClasses()
{
var serializedRefClasses = new[] { "FantasticAssembly:AwesomeNS.Foo", "FantasticAssembly:AwesomeNS.Bar", "SuperFantasticAssembly:SuperAwesomeNS.Bar"};
var link = new LinkXmlGenerator();
link.AddSerializedClass(serializedRefClasses);
link.Save(k_LinkFile);
var xml = ReadLinkXML(k_LinkFile, out int assemblyCount, out int typeCount);
Assert.AreEqual(assemblyCount, 2);
AssertAssemblyFullName(xml, "FantasticAssembly");
AssertAssemblyFullName(xml, "SuperFantasticAssembly");
Assert.AreEqual(typeCount,3);
AssertTypeWithAttributePreserved(xml, "AwesomeNS.Foo");
AssertTypeWithAttributePreserved(xml, "AwesomeNS.Bar");
AssertTypeWithAttributePreserved(xml, "SuperAwesomeNS.Bar");
}
[Test]
public void LinkXML_Is_Sorted()
{
// For incremental builds to perform well, we need link xml files
// to be deterministic - if contents don't change, they should be bit-wise
// identical. So we sort contents, since HashSets/Dictionary have no deterministic order.
// This test verfies that assemblies and classes are sorted alphabetically.
// Note that types added via AddSerializedClass will always come after types and assemblies
// added via AddType/AddAssembly, regardless of alphabetical order.
var serializedRefClasses = new[]
{
"AssemblyC:NamespaceA.ClassA",
"AssemblyB:NamespaceC.ClassB",
"AssemblyA:NamespaceB.ClassC",
"AssemblyC:NamespaceC.ClassC",
"AssemblyB:NamespaceB.ClassA",
"AssemblyA:NamespaceA.ClassB",
"AssemblyA:NamespaceA.ClassC",
"AssemblyC:NamespaceB.ClassA",
"AssemblyB:NamespaceC.ClassA",
};
var types = new[] { typeof(UnityEngine.Texture), typeof(UnityEngine.MonoBehaviour) , typeof(UnityEngine.Build.Pipeline.CompatibilityAssetBundleManifest) };
var link = new LinkXmlGenerator();
link.AddSerializedClass(serializedRefClasses);
link.AddTypes(types);
link.Save(k_LinkFile);
var xml = ReadLinkXML(k_LinkFile, out int assemblyCount, out int typeCount)
.Replace(" ", "")
.Replace("\n", "")
.Replace("\r", "");
Console.WriteLine(xml);
var sorted = @"
<linker>
<assembly fullname=""Unity.ScriptableBuildPipeline,Version=0.0.0.0,Culture=neutral,PublicKeyToken=null"">
<type fullname=""UnityEngine.Build.Pipeline.CompatibilityAssetBundleManifest"" preserve=""all"" />
</assembly>
<assembly fullname=""UnityEngine.CoreModule,Version=0.0.0.0,Culture=neutral,PublicKeyToken=null"">
<type fullname=""UnityEngine.MonoBehaviour"" preserve=""all"" />
<type fullname=""UnityEngine.Texture"" preserve=""all"" />
</assembly>
<assembly fullname=""AssemblyA"">
<type fullname=""NamespaceA.ClassB"" preserve=""nothing"" serialized=""true"" />
<type fullname=""NamespaceA.ClassC"" preserve=""nothing"" serialized=""true"" />
<type fullname=""NamespaceB.ClassC"" preserve=""nothing"" serialized=""true"" />
</assembly>
<assembly fullname=""AssemblyB"">
<type fullname=""NamespaceB.ClassA"" preserve=""nothing"" serialized=""true"" />
<type fullname=""NamespaceC.ClassA"" preserve=""nothing"" serialized=""true"" />
<type fullname=""NamespaceC.ClassB"" preserve=""nothing"" serialized=""true"" />
</assembly>
<assembly fullname=""AssemblyC"">
<type fullname=""NamespaceA.ClassA"" preserve=""nothing"" serialized=""true"" />
<type fullname=""NamespaceB.ClassA"" preserve=""nothing"" serialized=""true"" />
<type fullname=""NamespaceC.ClassC"" preserve=""nothing"" serialized=""true"" />
</assembly>
</linker>
".Replace(" ", "").Replace("\n", "").Replace("\r", "");
Assert.AreEqual(sorted, xml);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2030ed9ba38c2be4bbbc6a3499d8a4d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,9 @@
using NUnit.Framework;
namespace UnityEditor.Build.Pipeline.Tests
{
// Test suite against Build Cache, tests are implemented in BuildCacheTestBase
class LocalBuildCacheTests : BuildCacheTestBase
{
};
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3bf61af2f16576547894347350205854
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,146 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using UnityEditor;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
class LocalCacheServer : ScriptableSingleton<LocalCacheServer>
{
[SerializeField] public string m_path;
[SerializeField] public int m_port;
[SerializeField] public int m_pid = -1;
public static string Combine(params string[] components)
{
if (components.Length < 1)
throw new ArgumentException("At least one component must be provided!");
var path1 = components[0];
for (var index = 1; index < components.Length; ++index)
path1 = Path.Combine(path1, components[index]);
return path1;
}
private void Create(int port, ulong size, string cachePath)
{
var nodeExecutable = Combine(EditorApplication.applicationContentsPath, "Tools", "nodejs");
nodeExecutable = Application.platform == RuntimePlatform.WindowsEditor
? Combine(nodeExecutable, "node.exe")
: Combine(nodeExecutable, "bin", "node");
if (!Directory.Exists(cachePath))
Directory.CreateDirectory(cachePath);
m_path = cachePath;
var cacheServerJs = Combine(EditorApplication.applicationContentsPath, "Tools", "CacheServer", "main.js");
var processStartInfo = new ProcessStartInfo(nodeExecutable)
{
Arguments = "\"" + cacheServerJs + "\""
+ " --port " + port
+ " --path \"" + m_path
+ "\" --nolegacy"
+ " --monitor-parent-process " + Process.GetCurrentProcess().Id
// node.js has issues running on windows with stdout not redirected.
// so we silence logging to avoid that. And also to avoid CacheServer
// spamming the editor logs on OS X.
+ " --silent"
+ " --size " + size,
UseShellExecute = false,
CreateNoWindow = true
};
var p = new Process { StartInfo = processStartInfo };
p.Start();
m_port = port;
m_pid = p.Id;
Save(true);
}
public static string CachePath
{
get { return instance.m_path; }
}
public static int Port
{
get { return instance.m_port; }
}
public static void Setup(ulong size, string cachePath)
{
Kill();
instance.Create(GetRandomUnusedPort(), size, cachePath);
WaitForServerToComeAlive(instance.m_port);
}
public static void Kill()
{
if (instance.m_pid == -1)
return;
try
{
var p = Process.GetProcessById(instance.m_pid);
p.Kill();
instance.m_pid = -1;
}
catch
{
// if we could not get a process, there is non alive. continue.
}
}
public static void Clear()
{
Kill();
if (Directory.Exists(instance.m_path))
Directory.Delete(instance.m_path, true);
}
private static void WaitForServerToComeAlive(int port)
{
var start = DateTime.Now;
var maximum = start.AddSeconds(5);
while (DateTime.Now < maximum)
{
if (!PingHost("localhost", port, 10)) continue;
Console.WriteLine("Server Came alive after {0} ms", (DateTime.Now - start).TotalMilliseconds);
break;
}
}
private static int GetRandomUnusedPort()
{
var listener = new TcpListener(IPAddress.Any, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private static bool PingHost(string host, int port, int timeout)
{
try
{
using (var client = new TcpClient())
{
var result = client.BeginConnect(host, port, null, null);
result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(timeout));
return client.Connected;
}
}
catch
{
return false;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5b2102a42c1d8884989d74c98d4442b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.IO;
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.Utilities;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
class ObjectExtractionTaskTests
{
internal class ExtractionDependencyData : TestDependencyDataBase
{
Dictionary<GUID, AssetLoadInfo> m_AssetInfo = new Dictionary<GUID, AssetLoadInfo>();
public override Dictionary<GUID, AssetLoadInfo> AssetInfo => m_AssetInfo;
Dictionary<GUID, SceneDependencyInfo> m_SceneInfo = new Dictionary<GUID, SceneDependencyInfo>();
public override Dictionary<GUID, SceneDependencyInfo> SceneInfo => m_SceneInfo;
}
internal class ExtractionObjectLayout : TestBundleExplictObjectLayout
{
Dictionary<ObjectIdentifier, string> m_ExplicitObjectLocation = new Dictionary<ObjectIdentifier, string>();
public override Dictionary<ObjectIdentifier, string> ExplicitObjectLocation { get => m_ExplicitObjectLocation; }
}
static ObjectIdentifier MakeObjectId(string guid, long localIdentifierInFile, FileType fileType, string filePath)
{
var objectId = new ObjectIdentifier();
var boxed = (object)objectId;
var type = typeof(ObjectIdentifier);
type.GetField("m_GUID", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, new GUID(guid));
type.GetField("m_LocalIdentifierInFile", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, localIdentifierInFile);
type.GetField("m_FileType", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, fileType);
type.GetField("m_FilePath", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, filePath);
return (ObjectIdentifier)boxed;
}
static void TestSetup(out ObjectIdentifier nonMonoScriptId, out ObjectIdentifier monoScriptId, out CreateMonoScriptBundle task, out ExtractionDependencyData dependencyData, out ExtractionObjectLayout layout)
{
nonMonoScriptId = MakeObjectId(GUID.Generate().ToString(), 20, FileType.MetaAssetType, "");
monoScriptId = MakeObjectId(GUID.Generate().ToString(), 50, FileType.MetaAssetType, "");
task = new CreateMonoScriptBundle("MonoScriptBundle");
dependencyData = new ExtractionDependencyData();
layout = new ExtractionObjectLayout();
BuildCacheUtility.ClearCacheHashes();
BuildCacheUtility.SetTypeForObjects(new[] {
new ObjectTypes(nonMonoScriptId, new[] {typeof(GameObject) }),
new ObjectTypes(monoScriptId, new[] {typeof(MonoScript) })
});
IBuildContext context = new BuildContext(dependencyData, layout);
ContextInjector.Inject(context, task);
}
[Test]
public void CreateMonoScriptBundle_ExtractsMonoScripts_FromAssetInfo()
{
TestSetup(out ObjectIdentifier nonMonoScriptId, out ObjectIdentifier monoScriptId, out CreateMonoScriptBundle task, out ExtractionDependencyData dependencyData, out ExtractionObjectLayout layout);
var assetInfo = new AssetLoadInfo();
assetInfo.referencedObjects = new List<ObjectIdentifier> { nonMonoScriptId, monoScriptId };
dependencyData.AssetInfo.Add(GUID.Generate(), assetInfo);
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(nonMonoScriptId));
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(monoScriptId));
task.Run();
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(nonMonoScriptId));
Assert.IsTrue(layout.ExplicitObjectLocation.ContainsKey(monoScriptId));
Assert.AreEqual(task.MonoScriptBundleName, layout.ExplicitObjectLocation[monoScriptId]);
}
[Test]
public void CreateMonoScriptBundle_ExtractsMonoScripts_FromSceneInfo()
{
TestSetup(out ObjectIdentifier nonMonoScriptId, out ObjectIdentifier monoScriptId, out CreateMonoScriptBundle task, out ExtractionDependencyData dependencyData, out ExtractionObjectLayout layout);
var sceneInfo = new SceneDependencyInfo();
sceneInfo.SetReferencedObjects(new[] { nonMonoScriptId, monoScriptId });
dependencyData.SceneInfo.Add(GUID.Generate(), sceneInfo);
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(nonMonoScriptId));
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(monoScriptId));
task.Run();
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(nonMonoScriptId));
Assert.IsTrue(layout.ExplicitObjectLocation.ContainsKey(monoScriptId));
Assert.AreEqual(task.MonoScriptBundleName, layout.ExplicitObjectLocation[monoScriptId]);
}
[Test]
public void CreateMonoScriptBundle_DoesNotExtractMonoScripts_FromDefaultResources()
{
TestSetup(out ObjectIdentifier nonMonoScriptId, out ObjectIdentifier monoScriptId, out CreateMonoScriptBundle task, out ExtractionDependencyData dependencyData, out ExtractionObjectLayout layout);
var defaultResourceMonoScriptId = MakeObjectId(CommonStrings.UnityDefaultResourceGuid, 50, FileType.MetaAssetType, CommonStrings.UnityDefaultResourcePath);
BuildCacheUtility.SetTypeForObjects(new[] {
new ObjectTypes(defaultResourceMonoScriptId, new[] {typeof(MonoScript) })
});
var assetInfo = new AssetLoadInfo();
assetInfo.referencedObjects = new List<ObjectIdentifier> { nonMonoScriptId, monoScriptId, defaultResourceMonoScriptId };
dependencyData.AssetInfo.Add(GUID.Generate(), assetInfo);
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(nonMonoScriptId));
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(monoScriptId));
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(defaultResourceMonoScriptId));
task.Run();
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(nonMonoScriptId));
Assert.IsTrue(layout.ExplicitObjectLocation.ContainsKey(monoScriptId));
Assert.IsFalse(layout.ExplicitObjectLocation.ContainsKey(defaultResourceMonoScriptId));
Assert.AreEqual(task.MonoScriptBundleName, layout.ExplicitObjectLocation[monoScriptId]);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9311edf98412ecc45a1f6ce3bb6a9fea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Unity.Collections.LowLevel.Unsafe;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
class PrefabPackedStressTest
{
static ObjectIdentifier MakeObjectId(GUID guid, long localIdentifierInFile, FileType fileType, string filePath)
{
var objectId = new ObjectIdentifier();
var boxed = (object)objectId;
var type = typeof(ObjectIdentifier);
type.GetField("m_GUID", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, guid);
type.GetField("m_LocalIdentifierInFile", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, localIdentifierInFile);
type.GetField("m_FileType", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, fileType);
type.GetField("m_FilePath", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(boxed, filePath);
return (ObjectIdentifier)boxed;
}
static long LongRandom(System.Random rand)
{
byte[] buffer = new byte[8];
rand.NextBytes(buffer);
return BitConverter.ToInt64(buffer, 0);
}
static unsafe GUID GuidRandom(System.Random rand)
{
GUID guid = new GUID();
byte[] bytes = new byte[16];
rand.NextBytes(bytes);
fixed (byte* ptr = &bytes[0])
UnsafeUtility.MemCpy(&guid, ptr, bytes.Length);
return guid;
}
void SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, bool v2Hasher = false, int seed = 0, int header = 2)
{
#if UNITY_2020_1_OR_NEWER
prevV2Hasher = ScriptableBuildPipeline.useV2Hasher;
ScriptableBuildPipeline.useV2Hasher = v2Hasher;
#else
prevV2Hasher = false;
#endif
prevSeed = ScriptableBuildPipeline.fileIDHashSeed;
ScriptableBuildPipeline.fileIDHashSeed = seed;
prevHeader = ScriptableBuildPipeline.prefabPackedHeaderSize;
ScriptableBuildPipeline.prefabPackedHeaderSize = header;
}
void ResetSBP(bool prevV2Hasher, int prevSeed, int prevHeader)
{
#if UNITY_2020_1_OR_NEWER
ScriptableBuildPipeline.useV2Hasher = prevV2Hasher;
#endif
ScriptableBuildPipeline.fileIDHashSeed = prevSeed;
ScriptableBuildPipeline.prefabPackedHeaderSize = prevHeader;
}
// We want to ensure 1 million objects in a single asset is collision free with the default implementations we provide users.
static int LfidStressRunCount = 1000000;
// We want to ensure 1 object in a 1 million assets is collision free with the default implementations we provide users.
static int GuidStressRunCount = 1000000;
// We want to ensure that we have a low number of cluster collisions in a bundle as this ensures better loading performance.
// Ideally we want 0 collisions, but working with 64 bits in the way we do for build, that might be impossible without getting more collisions
// in the full lfid generation which will break a build, vs just slow downloading slightly.
static int BatchStressRunCount_Large = 10000;
static int BatchStressRunCount_Small = 100;
static int RandomSeed = 131072;
static ScriptableBuildPipeline.Settings DefaultSettings = new ScriptableBuildPipeline.Settings();
static object[] LfidStressCases =
{
new object[] { new PrefabPackedIdentifiers(), false, DefaultSettings.fileIDHashSeed, DefaultSettings.prefabPackedHeaderSize },
new object[] { new PrefabPackedIdentifiers(), true, DefaultSettings.fileIDHashSeed, DefaultSettings.prefabPackedHeaderSize }
};
[Test]
[TestCaseSource(nameof(LfidStressCases))]
public void SerializationIndexFromObjectIdentifier_Lfid_CollisionFreeStressTest(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize)
{
SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, useV2Hasher, seed, headerSize);
System.Random rand = new System.Random(RandomSeed);
ObjectIdentifier objId = MakeObjectId(GuidRandom(rand), 3867712242362628071, FileType.MetaAssetType, "");
Dictionary<long, ObjectIdentifier> consumedIds = new Dictionary<long, ObjectIdentifier>(LfidStressRunCount);
for (int i = 0; i < LfidStressRunCount; i++)
{
objId.SetLocalIdentifierInFile(LongRandom(rand));
long lfid = packingMethod.SerializationIndexFromObjectIdentifier(objId);
if (consumedIds.TryGetValue(lfid, out var prevObjId))
{
if (objId == prevObjId)
continue;
else
Assert.Fail($"{objId} with {prevObjId} at {lfid}");
}
consumedIds.Add(lfid, objId);
}
ResetSBP(prevV2Hasher, prevSeed, prevHeader);
}
[Test]
[TestCaseSource(nameof(LfidStressCases))]
public void SerializationIndexFromObjectIdentifier_GUID_CollisionFreeStressTest(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize)
{
SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, useV2Hasher, seed, headerSize);
System.Random rand = new System.Random(RandomSeed);
ObjectIdentifier objId = MakeObjectId(GuidRandom(rand), 3867712242362628071, FileType.MetaAssetType, "");
Dictionary<long, ObjectIdentifier> consumedIds = new Dictionary<long, ObjectIdentifier>(GuidStressRunCount);
for (int i = 0; i < GuidStressRunCount; i++)
{
objId.SetGuid(GuidRandom(rand));
long lfid = packingMethod.SerializationIndexFromObjectIdentifier(objId);
if (consumedIds.TryGetValue(lfid, out var prevObjId))
{
if (objId == prevObjId)
continue;
else
Assert.Fail($"{objId} with {prevObjId} at {lfid}");
}
consumedIds.Add(lfid, objId);
}
ResetSBP(prevV2Hasher, prevSeed, prevHeader);
}
[Test]
[TestCaseSource(nameof(LfidStressCases))]
public void SerializationIndexFromObjectIdentifier_BatchingQualityStressTest_Large(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize)
{
BatchingQualityStressTest(packingMethod, useV2Hasher, seed, headerSize, BatchStressRunCount_Large);
}
[Test]
[TestCaseSource(nameof(LfidStressCases))]
public void SerializationIndexFromObjectIdentifier_BatchingQualityStressTest_Small(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize)
{
BatchingQualityStressTest(packingMethod, useV2Hasher, seed, headerSize, BatchStressRunCount_Small);
}
void BatchingQualityStressTest(IDeterministicIdentifiers packingMethod, bool useV2Hasher, int seed, int headerSize, int runCount)
{
// This test is to check the quality of default clustering per source asset falls within certain guidelines
// For 10,000 unique assets and 100 unique assets, we want to ensure that at most we see <0.1% (10)
// assets generate the same cluster and <10% collision of all clusters.
SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, useV2Hasher, seed, headerSize);
System.Random rand = new System.Random(RandomSeed);
ObjectIdentifier objId = MakeObjectId(GuidRandom(rand), 3867712242362628071, FileType.MetaAssetType, "");
Dictionary<int, int> clusters = new Dictionary<int, int>();
for (int i = 0; i < runCount; i++)
{
objId.SetGuid(GuidRandom(rand));
long lfid = packingMethod.SerializationIndexFromObjectIdentifier(objId);
byte[] bytes = BitConverter.GetBytes(lfid);
byte[] header = new byte[4];
for (int j = 0; j < ScriptableBuildPipeline.prefabPackedHeaderSize; j++)
header[4 - ScriptableBuildPipeline.prefabPackedHeaderSize + j] = bytes[j];
int cluster = BitConverter.ToInt32(header, 0);
clusters.TryGetValue(cluster, out int count);
clusters[cluster] = count + 1;
}
int[] collisionValues = clusters.Values.Where(x => x > 1).ToArray();
Array.Sort(collisionValues);
int collisions = collisionValues.Length;
// Maximum assets per cluster with multiple assets, lower is better
int maxCollisions = collisionValues.Length > 0 ? collisionValues.Last() : 0;
// Median assets per cluster with multiple assets, lower is better
int medCollisions = collisionValues.Length > 0 ? collisionValues[collisionValues.Length / 2] : 0;
Debug.Log($"Reused Clusters {collisions} ({(float)collisions/runCount*100:n2}%), Max {maxCollisions} ({(float)maxCollisions/runCount*100}%), Med {medCollisions} ({(float)medCollisions/runCount*100}%)");
Assert.IsTrue(runCount * 0.1f > collisions, "Reused cluster count > 10%");
Assert.IsTrue(runCount * 0.001f > maxCollisions, "Max per cluster reuse > 0.1%");
ResetSBP(prevV2Hasher, prevSeed, prevHeader);
}
[Test]
public void CreateWriteCommand_ThrowsBuildException_OnCollision()
{
SetupSBP(out bool prevV2Hasher, out int prevSeed, out int prevHeader, false, 0, 4);
List<ObjectIdentifier> objectIds = new List<ObjectIdentifier>();
objectIds.Add(MakeObjectId(new GUID("066ce95d52fe15041854096a2145195e"), 3867712242362628071, FileType.MetaAssetType, ""));
objectIds.Add(MakeObjectId(new GUID("066ce95d52fe15041854096a2145195e"), 7498449973661844796, FileType.MetaAssetType, ""));
Assert.Throws(typeof(BuildFailedException), () => GenerateBundleCommands.CreateWriteCommand("InternalName", objectIds, new PrefabPackedIdentifiers()));
ResetSBP(prevV2Hasher, prevSeed, prevHeader);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 254069fb351a0914c94c4751c2ee9851
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,107 @@
using UnityEditor.Build.Content;
namespace UnityEditor.Build.Pipeline.Tests
{
internal static class ReflectionExtentions
{
public static void SetFileName(this ref ResourceFile file, string filename)
{
#if UNITY_2019_3_OR_NEWER
file.fileName = filename;
#else
var fieldInfo = typeof(ResourceFile).GetField("m_FileName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = file;
fieldInfo.SetValue(boxed, filename);
file = (ResourceFile)boxed;
#endif
}
public static void SetFileAlias(this ref ResourceFile file, string fileAlias)
{
#if UNITY_2019_3_OR_NEWER
file.fileAlias = fileAlias;
#else
var fieldInfo = typeof(ResourceFile).GetField("m_FileAlias", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = file;
fieldInfo.SetValue(boxed, fileAlias);
file = (ResourceFile)boxed;
#endif
}
public static void SetSerializedFile(this ref ResourceFile file, bool serializedFile)
{
#if UNITY_2019_3_OR_NEWER
file.serializedFile = serializedFile;
#else
var fieldInfo = typeof(ResourceFile).GetField("m_SerializedFile", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = file;
fieldInfo.SetValue(boxed, serializedFile);
file = (ResourceFile)boxed;
#endif
}
public static void SetHeader(this ref ObjectSerializedInfo osi, SerializedLocation serializedLocation)
{
var fieldInfo = typeof(ObjectSerializedInfo).GetField("m_Header", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = osi;
fieldInfo.SetValue(boxed, serializedLocation);
osi = (ObjectSerializedInfo)boxed;
}
public static void SetFileName(this ref SerializedLocation serializedLocation, string filename)
{
var fieldInfo = typeof(SerializedLocation).GetField("m_FileName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = serializedLocation;
fieldInfo.SetValue(boxed, filename);
serializedLocation = (SerializedLocation)boxed;
}
public static void SetOffset(this ref SerializedLocation serializedLocation, ulong offset)
{
var fieldInfo = typeof(SerializedLocation).GetField("m_Offset", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = serializedLocation;
fieldInfo.SetValue(boxed, offset);
serializedLocation = (SerializedLocation)boxed;
}
public static void SetObjectIdentifier(this ref ObjectIdentifier obj, GUID guid, long localIdentifierInFile, FileType fileType, string filePath)
{
SetGuid(ref obj, guid);
SetLocalIdentifierInFile(ref obj, localIdentifierInFile);
SetFileType(ref obj, fileType);
SetFilePath(ref obj, filePath);
}
public static void SetGuid(this ref ObjectIdentifier obj, GUID guid)
{
var fieldInfo = typeof(ObjectIdentifier).GetField("m_GUID", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = obj;
fieldInfo.SetValue(boxed, guid);
obj = (ObjectIdentifier)boxed;
}
public static void SetLocalIdentifierInFile(this ref ObjectIdentifier obj, long localIdentifierInFile)
{
var fieldInfo = typeof(ObjectIdentifier).GetField("m_LocalIdentifierInFile", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = obj;
fieldInfo.SetValue(boxed, localIdentifierInFile);
obj = (ObjectIdentifier)boxed;
}
public static void SetFileType(this ref ObjectIdentifier obj, FileType fileType)
{
var fieldInfo = typeof(ObjectIdentifier).GetField("m_FileType", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = obj;
fieldInfo.SetValue(boxed, fileType);
obj = (ObjectIdentifier)boxed;
}
public static void SetFilePath(this ref ObjectIdentifier obj, string filePath)
{
var fieldInfo = typeof(ObjectIdentifier).GetField("m_FilePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
object boxed = obj;
fieldInfo.SetValue(boxed, filePath);
obj = (ObjectIdentifier)boxed;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee92aa36338b87445ac230ed769ea068
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,637 @@
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<T>(params IContextObject[] args) where T : IBuildTask
{
IBuildContext context = new BuildContext(args);
IBuildTask instance = Activator.CreateInstance<T>();
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<AssetBundleBuild> buildData = new List<AssetBundleBuild>();
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<Assembly>();
var types = new HashSet<Type>();
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<WriteSerializedFiles>(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<GenerateBundlePacking>(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<GenerateBundlePacking>(buildContent, dep, writeData, deterministicId);
IBundleBuildParameters buildParams = GetBuildParameters();
ReturnCode exitCode = RunTask<GenerateBundleCommands>(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<GenerateBundleMaps>(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<PostPackingCallback>(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<PostWritingCallback>(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<PostDependencyCallback>(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<PostScriptsCallback>(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<AppendBundleHash>(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<GameObject>(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<GameObject>(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;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 170ac2c906650624cbab749adb2cc2ed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,194 @@
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
using static UnityEditor.Build.Pipeline.Utilities.TaskCachingUtility;
namespace UnityEditor.Build.Pipeline.Tests
{
public class TaskCachingUtilityTests
{
class ItemContext
{
public ItemContext(int input) { this.input = input; }
public int input;
public int result;
}
class FakeTracker : IProgressTracker
{
public bool shouldCancel;
int IProgressTracker.TaskCount { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
float IProgressTracker.Progress => throw new NotImplementedException();
bool IProgressTracker.UpdateInfo(string taskInfo)
{
return !shouldCancel;
}
bool IProgressTracker.UpdateTask(string taskTitle)
{
throw new NotImplementedException();
}
}
class TestRunCachedCallbacks<T> : IRunCachedCallbacks<T>
{
public int CreateCachedInfoCount;
public Func<WorkItem<T>, CachedInfo> CreateCachedInfoCB;
public int CreateCacheEntryCount;
public Func<WorkItem<T>, CacheEntry> CreateCacheEntryCB;
public int PostProcessCount;
public Action<WorkItem<T>> PostProcessCB;
public int ProcessCachedCount;
public Action<WorkItem<T>, CachedInfo> ProcessCachedCB;
public int ProcessUncachedCount;
public Action<WorkItem<T>> ProcessUncachedCB;
public void ClearCounts()
{
CreateCachedInfoCount = 0;
CreateCacheEntryCount = 0;
PostProcessCount = 0;
ProcessCachedCount = 0;
ProcessUncachedCount = 0;
}
public CachedInfo CreateCachedInfo(WorkItem<T> item)
{
CreateCachedInfoCount++;
return CreateCachedInfoCB(item);
}
public CacheEntry CreateCacheEntry(WorkItem<T> item)
{
CreateCacheEntryCount++;
return CreateCacheEntryCB(item);
}
public void PostProcess(WorkItem<T> item)
{
PostProcessCount++;
PostProcessCB(item);
}
public void ProcessCached(WorkItem<T> item, CachedInfo info)
{
ProcessCachedCount++;
ProcessCachedCB(item, info);
}
public void ProcessUncached(WorkItem<T> item)
{
ProcessUncachedCount++;
ProcessUncachedCB(item);
}
}
[Test]
public void RunCachedOperation_WhenSomeItemsCached_UncachedItemsAreProcessed()
{
Func<int, int> transform = (x) => x * 27;
const int kFirstPassCount = 5;
const int kSecondPassCount = 12;
Dictionary<int, int> indexToResult = new Dictionary<int, int>();
Action<int> AssertResults = (count) =>
{
Assert.AreEqual(count, indexToResult.Count);
indexToResult.Keys.ToList().ForEach(x => Assert.AreEqual(transform(x), indexToResult[x]));
};
List<int> pass1Inputs = Enumerable.Range(0, kFirstPassCount).Select(x => x * 2).ToList();
List<int> pass2Inputs = Enumerable.Range(0, kSecondPassCount).ToList();
List<WorkItem<ItemContext>> workerInput1 =
pass1Inputs.Select(i => new WorkItem<ItemContext>(new ItemContext(i))).ToList();
List<WorkItem<ItemContext>> workerInput2 =
pass2Inputs.Select(i => new WorkItem<ItemContext>(new ItemContext(i))).ToList();
TestRunCachedCallbacks<ItemContext> callbacks = new TestRunCachedCallbacks<ItemContext>();
callbacks.CreateCacheEntryCB = (item) =>
{
return new CacheEntry()
{
Guid = HashingMethods.Calculate("Test").ToGUID(),
Hash = HashingMethods.Calculate(item.Context.input).ToHash128(),
Type = CacheEntry.EntryType.Data
};
};
callbacks.ProcessUncachedCB = (item) => item.Context.result = transform(item.Context.input);
callbacks.ProcessCachedCB = (item, info) => item.Context.result = (int)info.Data[0];
callbacks.PostProcessCB = (item) => indexToResult.Add(item.Context.input, item.Context.result);
callbacks.CreateCachedInfoCB = (item) =>
{
return new CachedInfo() { Data = new object[] { item.Context.result }, Dependencies = new CacheEntry[0], Asset = item.entry };
};
BuildCache.PurgeCache(false);
using (BuildCache cache = new BuildCache())
{
TaskCachingUtility.RunCachedOperation<ItemContext>(cache, null, null, workerInput1, callbacks);
AssertResults(workerInput1.Count);
indexToResult.Clear();
Assert.AreEqual(kFirstPassCount, callbacks.CreateCacheEntryCount);
Assert.AreEqual(kFirstPassCount, callbacks.PostProcessCount);
Assert.AreEqual(0, callbacks.ProcessCachedCount);
Assert.AreEqual(kFirstPassCount, callbacks.ProcessUncachedCount);
Assert.AreEqual(kFirstPassCount, callbacks.CreateCachedInfoCount);
callbacks.ClearCounts();
cache.SyncPendingSaves();
TaskCachingUtility.RunCachedOperation<ItemContext>(cache, null, null, workerInput2, callbacks);
AssertResults(workerInput2.Count);
Assert.AreEqual(kSecondPassCount, callbacks.CreateCacheEntryCount);
Assert.AreEqual(kSecondPassCount, callbacks.PostProcessCount);
Assert.AreEqual(kFirstPassCount, callbacks.ProcessCachedCount);
Assert.AreEqual(kSecondPassCount - kFirstPassCount, callbacks.ProcessUncachedCount);
Assert.AreEqual(kSecondPassCount - kFirstPassCount, callbacks.CreateCachedInfoCount);
}
}
[Test]
public void RunCachedOperation_WhenCancelled_DoesNotContinueProcessing()
{
TestRunCachedCallbacks<int> callbacks = new TestRunCachedCallbacks<int>();
List<WorkItem<int>> list = Enumerable.Range(0, 2).
Select(i => new WorkItem<int>(i)).ToList();
FakeTracker tracker = new FakeTracker();
callbacks.ProcessUncachedCB = (item) => { tracker.shouldCancel = true; };
callbacks.PostProcessCB = (item) => {};
ReturnCode code = TaskCachingUtility.RunCachedOperation<int>(null, null, tracker, list, callbacks);
Assert.AreEqual(1, callbacks.ProcessUncachedCount);
Assert.AreEqual(code, ReturnCode.Canceled);
}
[Test]
public void RunCachedOperation_WhenNoCache_ProcessesAllItems()
{
const int kIterations = 5;
TestRunCachedCallbacks<ItemContext> callbacks = new TestRunCachedCallbacks<ItemContext>();
Dictionary<int, int> indexToResult = new Dictionary<int, int>();
List<WorkItem<ItemContext>> list = Enumerable.Range(0, kIterations).
Select(i => new WorkItem<ItemContext>(new ItemContext(i * 10))).ToList();
callbacks.ProcessUncachedCB = (item) => item.Context.result = item.Context.input * 100;
callbacks.PostProcessCB = (item) => indexToResult.Add(item.Context.input, item.Context.result);
TaskCachingUtility.RunCachedOperation<ItemContext>(null, null, null, list, callbacks);
Assert.AreEqual(5, callbacks.PostProcessCount);
Assert.AreEqual(5, callbacks.ProcessUncachedCount);
foreach (WorkItem<ItemContext> item in list)
Assert.AreEqual(item.Context.result, indexToResult[item.Context.input]);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c2dcb086cc041f2459f8cb57dee4d25f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,131 @@
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEditor.Build.Player;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
internal static class TestTracing
{
public static string Callsite([System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
return $"at {memberName} in {sourceFilePath}:{sourceLineNumber}";
}
}
internal class TestBuildParametersBase : IBuildParameters
{
public virtual BuildTarget Target { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual BuildTargetGroup Group { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual ContentBuildFlags ContentBuildFlags { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual TypeDB ScriptInfo { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual ScriptCompilationOptions ScriptOptions { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual string TempOutputFolder { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual string ScriptOutputFolder { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual bool UseCache { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual string CacheServerHost { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual int CacheServerPort { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual bool WriteLinkXML { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual bool NonRecursiveDependencies { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual UnityEngine.BuildCompression GetCompressionForIdentifier(string identifier)
{
throw new System.NotImplementedException(TestTracing.Callsite());
}
public virtual BuildSettings GetContentBuildSettings()
{
throw new System.NotImplementedException(TestTracing.Callsite());
}
public virtual string GetOutputFilePathForIdentifier(string identifier)
{
throw new System.NotImplementedException(TestTracing.Callsite());
}
public virtual ScriptCompilationSettings GetScriptCompilationSettings()
{
throw new System.NotImplementedException(TestTracing.Callsite());
}
}
internal class TestBundleBuildParameters : TestBuildParametersBase, IBundleBuildParameters
{
public virtual bool AppendHash { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public virtual bool ContiguousBundles { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public virtual bool DisableVisibleSubAssetRepresentations { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
}
internal class TestBundleBuildContent : IBundleBuildContent
{
public virtual Dictionary<string, List<GUID>> BundleLayout => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual Dictionary<GUID, string> Addresses => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual List<GUID> Assets => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual List<GUID> Scenes => throw new System.NotImplementedException(TestTracing.Callsite());
#if UNITY_2019_3_OR_NEWER
public virtual Dictionary<string, List<ResourceFile>> AdditionalFiles => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual List<CustomContent> CustomAssets => throw new System.NotImplementedException(TestTracing.Callsite());
#endif
}
internal class TestDependencyDataBase : IDependencyData
{
public virtual Dictionary<GUID, AssetLoadInfo> AssetInfo => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual Dictionary<GUID, BuildUsageTagSet> AssetUsage => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual Dictionary<GUID, SceneDependencyInfo> SceneInfo => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual Dictionary<GUID, BuildUsageTagSet> SceneUsage => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual BuildUsageCache DependencyUsageCache => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual BuildUsageTagGlobal GlobalUsage { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public virtual Dictionary<GUID, Hash128> DependencyHash => throw new System.NotImplementedException(TestTracing.Callsite());
}
internal class TestWriteDataBase : IWriteData
{
public virtual Dictionary<GUID, List<string>> AssetToFiles => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual Dictionary<string, List<ObjectIdentifier>> FileToObjects => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual List<IWriteOperation> WriteOperations => throw new System.NotImplementedException(TestTracing.Callsite());
}
internal class TestBuildResultsBase : IBuildResults
{
public virtual ScriptCompilationResult ScriptResults { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
public virtual Dictionary<string, WriteResult> WriteResults => throw new System.NotImplementedException(TestTracing.Callsite());
public virtual Dictionary<string, SerializedFileMetaData> WriteResultsMetaData => throw new System.NotImplementedException(TestTracing.Callsite());
public Dictionary<GUID, AssetResultData> AssetResults => throw new System.NotImplementedException(TestTracing.Callsite());
}
internal class TestBundleExplictObjectLayout : IBundleExplictObjectLayout
{
public virtual Dictionary<ObjectIdentifier, string> ExplicitObjectLocation { get => throw new System.NotImplementedException(TestTracing.Callsite()); set => throw new System.NotImplementedException(TestTracing.Callsite()); }
}
internal class TestBundleExtendedAssetData : IBuildExtendedAssetData
{
public virtual Dictionary<GUID, ExtendedAssetData> ExtendedData => throw new System.NotImplementedException();
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e8cc8b5af2f62e418573550ab569877
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,144 @@
using System.Collections.Concurrent;
using System.Threading;
using NUnit.Framework;
namespace UnityEditor.Build.Pipeline.Tests
{
[TestFixture]
public class ThreadingManagerTasks
{
[TestCase(ThreadingManager.ThreadQueues.SaveQueue)]
[TestCase(ThreadingManager.ThreadQueues.UploadQueue)]
[TestCase(ThreadingManager.ThreadQueues.PruneQueue)]
public void ThreadingTasks_SameQueues_ExecuteSequentially(object queue)
{
var testQueue = (ThreadingManager.ThreadQueues)queue;
SemaphoreSlim s1 = new SemaphoreSlim(0);
SemaphoreSlim s2 = new SemaphoreSlim(0);
ConcurrentQueue<int> callOrder = new ConcurrentQueue<int>();
ThreadingManager.QueueTask(testQueue, (_) =>
{
s1.Wait();
callOrder.Enqueue(1);
Thread.Sleep(50);
callOrder.Enqueue(2);
}, null);
ThreadingManager.QueueTask(testQueue, (_) =>
{
callOrder.Enqueue(3);
}, null);
Thread.Sleep(100);
s1.Release();
ThreadingManager.WaitForOutstandingTasks();
Assert.AreEqual(3, callOrder.Count);
CollectionAssert.AreEqual(new[] { 1, 2, 3 }, callOrder.ToArray());
}
[Test]
public void ThreadingTasks_SaveQueue_RunsInParallelWith_UploadQueue()
{
SemaphoreSlim s1 = new SemaphoreSlim(0);
SemaphoreSlim s2 = new SemaphoreSlim(0);
SemaphoreSlim s3 = new SemaphoreSlim(0);
ConcurrentQueue<int> callOrder = new ConcurrentQueue<int>();
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.SaveQueue, (_) =>
{
s1.Wait();
callOrder.Enqueue(1);
s2.Release();
s3.Wait();
callOrder.Enqueue(2);
}, null);
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.UploadQueue, (_) =>
{
s2.Wait();
callOrder.Enqueue(3);
s3.Release();
}, null);
Thread.Sleep(100);
s1.Release();
ThreadingManager.WaitForOutstandingTasks();
Assert.AreEqual(3, callOrder.Count);
CollectionAssert.AreEqual(new[] { 1, 3, 2 }, callOrder.ToArray());
}
[Test]
public void ThreadingTasks_PruneQueue_RunsAfter_SaveAndUploadQueue()
{
SemaphoreSlim s1 = new SemaphoreSlim(0);
SemaphoreSlim s2 = new SemaphoreSlim(0);
SemaphoreSlim s3 = new SemaphoreSlim(0);
ConcurrentQueue<int> callOrder = new ConcurrentQueue<int>();
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.SaveQueue, (_) =>
{
s1.Wait();
callOrder.Enqueue(1);
s2.Release();
s3.Wait();
callOrder.Enqueue(2);
}, null);
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.UploadQueue, (_) =>
{
s2.Wait();
callOrder.Enqueue(3);
s3.Release();
}, null);
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.PruneQueue, (_) =>
{
callOrder.Enqueue(4);
}, null);
Thread.Sleep(100);
s1.Release();
ThreadingManager.WaitForOutstandingTasks();
Assert.AreEqual(4, callOrder.Count);
CollectionAssert.AreEqual(new[] { 1, 3, 2, 4 }, callOrder.ToArray());
}
[Test]
public void WhenPruneTaskActive_SaveAndUploadTasksWaitForPruneCompletion()
{
SemaphoreSlim s1 = new SemaphoreSlim(0);
ConcurrentQueue<int> callOrder = new ConcurrentQueue<int>();
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.PruneQueue, (_) =>
{
s1.Wait();
callOrder.Enqueue(1);
}, null);
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.SaveQueue, (_) =>
{
callOrder.Enqueue(2);
}, null);
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.UploadQueue, (_) =>
{
callOrder.Enqueue(3);
}, null);
Thread.Sleep(100);
s1.Release();
ThreadingManager.WaitForOutstandingTasks();
Assert.AreEqual(3, callOrder.Count);
Assert.IsTrue(callOrder.TryDequeue(out int result));
Assert.AreEqual(1, result);
var results = callOrder.ToArray();
CollectionAssert.AreEquivalent(new[] { 2, 3 }, results);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb09304cefcaac544889b26c496a2cac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,767 @@
// Define this to cause the before and after serialization DumpToText output for objects being tested to be echoed to the console.
// Slows down tests (some considerably) but allows visual inspection of the data which can be helpful for debugging issues
// #define DUMP_DATA_TEXT_TO_CONSOLE
using System.IO;
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEngine.Build.Pipeline;
using UnityEditor.Build.Pipeline.Tests;
using System.Reflection;
namespace UnityEditor.Build.Pipeline.Utilities.USerialize.Tests
{
// Tests for the USerialize serialization code
[TestFixture]
class USerializeTests
{
// --------------------------------------------------------------------------------------------------------------------------------------------------------------
// Test Cases
// --------------------------------------------------------------------------------------------------------------------------------------------------------------
// Test 'bool' is serialized/deserialized correctly
[TestCase(true)]
[TestCase(false)]
public void TestBoolSerializes(bool testValue) => TestSerializeData(new PrimitiveValue<bool>() { m_Value = testValue });
// Test 'Int32' is serialized/deserialized correctly
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
[TestCase(0)]
[TestCase(1234)]
public void TestInt32Serializes(int testValue) => TestSerializeData(new PrimitiveValue<int>() { m_Value = testValue });
// Test 'UInt32' is serialized/deserialized correctly
[TestCase(uint.MinValue)]
[TestCase(uint.MaxValue)]
[TestCase(0u)]
[TestCase(1234u)]
public void TestUInt32Serializes(uint testValue) => TestSerializeData(new PrimitiveValue<uint>() { m_Value = testValue });
// Test 'Int64' is serialized/deserialized correctly
[TestCase(long.MinValue)]
[TestCase(long.MaxValue)]
[TestCase(0)]
[TestCase(1234)]
public void TestInt64Serializes(long testValue) => TestSerializeData(new PrimitiveValue<long>() { m_Value = testValue });
// Test 'UInt64' is serialized/deserialized correctly
[TestCase(ulong.MinValue)]
[TestCase(ulong.MaxValue)]
[TestCase(0u)]
[TestCase(1234u)]
public void TestUInt64Serializes(ulong testValue) => TestSerializeData(new PrimitiveValue<ulong>() { m_Value = testValue });
// Test trying to serialize an unsupported primitive type throws
[Test]
public void TestSerializingUnsupportedPrimitiveTypeThrows()
{
Assert.Throws(typeof(InvalidDataException), () => TestSerializeData(new PrimitiveValue<float>() { m_Value = 123.4f }));
}
// Test trying to serialize an unsupported primitive array type throws
[Test]
public void TestSerializingUnsupportedArrayPrimitiveTypeThrows()
{
Assert.Throws(typeof(InvalidDataException), () => TestSerializeData(new PrimitiveValue<ClassWithUnsupportedPrimitiveArrayType>() { m_Value = new ClassWithUnsupportedPrimitiveArrayType() }));
}
// Test trying to serialize an array of rank greater than one throws
[Test]
public void TestSerializingUnsupportedArrayRankThrows()
{
Assert.Throws(typeof(InvalidDataException), () => TestSerializeData(new PrimitiveValue<int[,]>() { m_Value = new int[1, 1] }));
}
// Test writing string indices gives us back the values we expect and uses the expected number of bytes
[TestCase(0, 2)]
[TestCase(0x7FFF, 2)]
[TestCase(0x8000, 4)]
[TestCase(0x8001, 4)]
[TestCase(0x12345678, 4)]
[TestCase(USerialize.InvalidStringIndex, 4)]
public void TestWriteReadStringIndices(int stringIndex, int expectedSizeBytes)
{
using (MemoryStream stream = new MemoryStream())
{
// Write string index checking we wrote the expected number of bytes
Serializer serializer = new Serializer();
serializer.StartWritingToStream(stream);
long streamPosBeforeWriting = stream.Position;
serializer.WriteStringIndex(stringIndex);
serializer.FinishWritingToStream();
int numBytesWritten = (int)(stream.Position - streamPosBeforeWriting);
Assert.AreEqual(expectedSizeBytes, numBytesWritten);
// Read back the string index checking we got back what we expected and read the expected number of bytes
stream.Position = 0;
DeSerializer deserializer = new DeSerializer();
deserializer.StartReadingFromStream(stream);
long streamPosBeforeReading = stream.Position;
int readStringIndex = deserializer.ReadStringIndex();
int numBytesRead = (int)(stream.Position - streamPosBeforeReading);
Assert.AreEqual(expectedSizeBytes, numBytesRead);
Assert.AreEqual(stringIndex, readStringIndex);
}
}
// Test 'Hash128' is serialized/deserialized correctly
[TestCase(0x0123456789ABCDEFU, 0x94F8A3B87F327E2CU)]
public void TestHash128Serializes(ulong hashPart1, ulong hashPart2)
{
Hash128 testValue = new Hash128(hashPart1, hashPart2);
TestSerializeData(new PrimitiveValue<Hash128>() { m_Value = testValue });
}
// Test 'GUID' is serialized/deserialized correctly
[TestCase("ABD386FE297853ADF2397542BC3EA875")]
public void TestGUIDSerializes(string guidString)
{
GUID testValue = new GUID(guidString);
TestSerializeData(new PrimitiveValue<GUID>() { m_Value = testValue });
}
static readonly string SampleUnicodeString = "A Unicode string containing characters outside the ASCII code range, Pi (\u03a0) and Sigma (\u03a3) and a surrogate pair (\ud83e\udd70)";
// "The world is a better place with more creators in it" in various languages to exercise international characters. (done with Google translate, apologies for errors)
static readonly string BetterPlaceArabic = "\u0627\u0644\u0639\u0627\u0644\u0645\u0020\u0645\u0643\u0627\u0646\u0020\u0623\u0641\u0636\u0644\u0020\u0628\u0647\u0020\u0639\u062f\u062f\u0020\u0623\u0643\u0628\u0631\u0020\u0645\u0646\u0020\u0627\u0644\u0645\u0628\u062f\u0639\u064a\u0646";
static readonly string BetterPlaceHindi = "\u0907\u0938\u092e\u0947\u0902\u0020\u0905\u0927\u093f\u0915\u0020\u0930\u091a\u0928\u093e\u0915\u093e\u0930\u094b\u0902\u0020\u0915\u0947\u0020\u0938\u093e\u0925\u0020\u0926\u0941\u0928\u093f\u092f\u093e\u0020\u090f\u0915\u0020\u092c\u0947\u0939\u0924\u0930\u0020\u0938\u094d\u0925\u093e\u0928\u0020\u0939\u0948";
static readonly string BetterPlaceRussian = "\u043c\u0438\u0440\u0020\u0441\u0442\u0430\u043b\u0020\u043b\u0443\u0447\u0448\u0435\u002c\u0020\u0432\u0020\u043d\u0435\u043c\u0020\u0431\u043e\u043b\u044c\u0448\u0435\u0020\u0442\u0432\u043e\u0440\u0446\u043e\u0432";
static readonly string BetterPlaceIcelandic = "\u0068\u0065\u0069\u006d\u0075\u0072\u0069\u006e\u006e\u0020\u0065\u0072\u0020\u0062\u0065\u0074\u0072\u0069\u0020\u0073\u0074\u0061\u00f0\u0075\u0072\u0020\u006d\u0065\u00f0\u0020\u0066\u006c\u0065\u0069\u0072\u0069\u0020\u0068\u00f6\u0066\u0075\u006e\u0064\u0075\u006d\u0020\u00ed\u0020\u0068\u006f\u006e\u0075\u006d";
static readonly string BetterPlaceJapanese = "\u4e16\u754c\u306f\u3088\u308a\u591a\u304f\u306e\u30af\u30ea\u30a8\u30a4\u30bf\u30fc\u304c\u3044\u308b\u3088\u308a\u826f\u3044\u5834\u6240\u3067\u3059";
// Test strings of various forms are serialized/deserialized correctly
public static IEnumerable<string> TestStringsSerialize_TestCases()
{
yield return null;
yield return "";
yield return "\n";
yield return "FooBar";
yield return "\"FooBarQuotes\"";
yield return "\\FooBarBackslash\\";
yield return "Multi\nLine\nString";
yield return SampleUnicodeString;
yield return BetterPlaceArabic;
yield return BetterPlaceHindi;
yield return BetterPlaceRussian;
yield return BetterPlaceIcelandic;
yield return BetterPlaceJapanese;
}
[TestCaseSource("TestStringsSerialize_TestCases")]
public void TestStringsSerialize(string testValue) => TestSerializeData(new PrimitiveValue<string>() { m_Value = testValue });
// Test string arrays of various forms are serialized/deserialized correctly
public static IEnumerable<string[]> TestStringArraysSerialize_TestCases()
{
yield return null;
yield return new string[] { "" };
yield return new string[] { "foo" };
yield return new string[] { "foo", "bar", "multi\nline", "table\\backslash" };
yield return TestStringsSerialize_TestCases().ToArray();
}
[TestCaseSource("TestStringArraysSerialize_TestCases")]
public void TestStringArraysSerialize(string[] stringArray)
{
TestSerializeData(new PrimitiveArray<string>() { m_ArrayElements = stringArray });
}
// Test serializing a string array with more than 32,767 unique entries to ensure the standard 15bit string table index
// is insufficient and the fallback 31bit stringtable index encoding has to be used for some of the strings
[Test]
public void TestStringIndicesLargerThan15bit()
{
string[] testStrings = new string[40000];
for (int elementNum = 0; elementNum < testStrings.Length; elementNum++)
testStrings[elementNum] = "Test_" + elementNum;
TestSerializeData(new PrimitiveArray<string>() { m_ArrayElements = testStrings });
}
// Test byte arrays of various forms are serialized/deserialized correctly
[TestCase(null)]
[TestCase(new byte[0])]
[TestCase(new byte[] { 1, 2, 3, 4, 5 })]
public void TestByteArraysSerialize(byte[] testValue) => TestSerializeData(new PrimitiveArray<byte>() { m_ArrayElements = testValue });
// Test ulong arrays of various forms are serialized/deserialized correctly
[TestCase(null)]
[TestCase(new ulong[0])]
[TestCase(new ulong[] { 1, 2, 3, 4, 5 })]
[TestCase(new ulong[] { ulong.MinValue, 0, 1, ulong.MaxValue })]
public void TestULongArraysSerialize(ulong[] testValue) => TestSerializeData(new PrimitiveArray<ulong>() { m_ArrayElements = testValue });
// Test Type arrays of various forms are serialized/deserialized correctly
public static IEnumerable<Type[]> TestTypeArraysSerialize_TestCases()
{
yield return null;
yield return new Type[0];
yield return new Type[] { typeof(int), typeof(uint), typeof(float), typeof(CachedInfo), typeof(MonoBehaviour) };
yield return new Type[] { typeof(int), null, typeof(float), null, typeof(MonoBehaviour) };
}
[TestCaseSource("TestTypeArraysSerialize_TestCases")]
public void TestTypeArraysSerialize(Type[] testValue) => TestSerializeData(new PrimitiveArray<Type>() { m_ArrayElements = testValue });
// Test structs are serialized/deserialized correctly
[TestCase(-1)]
[TestCase(0)]
[TestCase(1)]
public void TestLinearStructsSerialize(int scalar)
{
LinearStruct value = new LinearStruct(scalar);
if (scalar != -1)
value.m_LinearClass = new LinearClass(scalar * 2);
TestSerializeData(value);
}
// Test classes are serialized/deserialized correctly
[Test]
public void TestLinearClassSerializes() => TestSerializeData(new PrimitiveValue<LinearClass>() { m_Value = new LinearClass(1) });
// Test the DeSerializer.ObjectVersion property returns the correct value after deserialization
[Test]
public void TestClientObjectVersionPropertyReturnedCorrectly()
{
using (MemoryStream stream = new MemoryStream())
{
// Our data
PrimitiveValue<int> data = new PrimitiveValue<int>() { m_Value = 3149123 };
const int objectVersion = 12345678;
// Write to stream
Serializer serializer = new Serializer();
serializer.Serialize(stream, data, objectVersion);
// Read from stream
stream.Position = 0;
DeSerializer deserializer = new DeSerializer();
PrimitiveValue<int> deserializedData = deserializer.DeSerialize<PrimitiveValue<int>>(stream);
// Test object contents and version is as expected
Assert.AreEqual(objectVersion, deserializer.ObjectVersion);
Assert.AreEqual(data.m_Value, deserializedData.m_Value);
}
}
// Test that CachedInfo instances serialize/deserialize correctly
[Test]
public void TestCachedInfoSerialises()
{
CachedInfo cachedInfo = CreateSyntheticCachedInfo(12478324);
TestSerializeData(new PrimitiveValue<CachedInfo>() { m_Value = cachedInfo },
BuildCache.CustomSerializers,
BuildCache.ObjectFactories,
new DumpToText.ICustomDumper[] { new CustomDumper_BuildUsageTagSet() },
false); // full Equality not implemented for CachedInfo so we rely on the dumper text comparison
}
// --------------------------------------------------------------------------------------------------------------------------------------------------------------
// Utilities & Supporting Code
// --------------------------------------------------------------------------------------------------------------------------------------------------------------
// Wrapper for a value that provides an IEquatable interface so it can be compared post-serialization-deserialization to the original value
class PrimitiveValue<ValueType> : IEquatable<PrimitiveValue<ValueType>>
{
internal ValueType m_Value;
public bool Equals(PrimitiveValue<ValueType> other)
{
return (other == null) ? false : (EqualityComparer<ValueType>.Default.Equals(m_Value, other.m_Value));
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals(obj as PrimitiveValue<ValueType>);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
// Wrapper for an array of primitive values that provides an IEquatable interface so it can be compared post-serialization-deserialization to the original value
class PrimitiveArray<ValueType> : IEquatable<PrimitiveArray<ValueType>>
{
internal ValueType[] m_ArrayElements;
public bool Equals(PrimitiveArray<ValueType> other)
{
if (other == null)
return false;
return (m_ArrayElements == null) ? (other.m_ArrayElements == null) : m_ArrayElements.SequenceEqual(other.m_ArrayElements);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals(obj as PrimitiveArray<ValueType>);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
// A struct of various primitive and reference types that provides an IEquatable interface so it can be compared post-serialization-deserialization to the original value
internal struct LinearStruct : IEquatable<LinearStruct>
{
internal bool m_Bool;
internal byte m_Byte;
internal int m_Int;
internal uint m_UInt;
internal long m_Long;
internal ulong m_ULong;
internal string m_String;
internal LinearClass m_LinearClass;
internal LinearStruct(int scalar)
{
m_Bool = ((scalar & 1) == 0);
m_Byte = (byte)scalar;
m_Int = 2 * scalar;
m_UInt = (uint)(3 * scalar);
m_Long = (long)(4 * scalar);
m_ULong = (ulong)(5 * scalar);
m_String = "(" + scalar.ToString() + ")";
m_LinearClass = null;
}
public bool Equals(LinearStruct other)
{
return (m_Bool == other.m_Bool) && (m_Byte == other.m_Byte) && (m_Int == other.m_Int) && (m_UInt == other.m_UInt) && (m_Long == other.m_Long) && (m_ULong == other.m_ULong) && (m_String == other.m_String) && (System.Object.Equals(m_LinearClass, other.m_LinearClass));
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
// A class of various primitive and reference types that provides an IEquatable interface so it can be compared post-serialization-deserialization to the original value
internal class LinearClass : IEquatable<LinearClass>
{
internal bool m_Bool;
internal byte m_Byte;
internal int m_Int;
internal uint m_UInt;
internal long m_Long;
internal ulong m_ULong;
internal string m_String;
internal LinearClass m_LinearClass;
internal LinearStruct m_LinearStruct;
public LinearClass()
{
}
internal LinearClass(int scalar)
{
m_Bool = ((scalar & 1) == 0);
m_Byte = (byte)scalar;
m_Int = 2 * scalar;
m_UInt = (uint)(3 * scalar);
m_Long = (long)(4 * scalar);
m_ULong = (ulong)(5 * scalar);
m_String = "(" + scalar.ToString() + ")";
m_LinearStruct = new LinearStruct(scalar * 10);
}
public bool Equals(LinearClass other)
{
return (m_Bool == other.m_Bool) && (m_Byte == other.m_Byte) && (m_Int == other.m_Int) && (m_UInt == other.m_UInt) && (m_Long == other.m_Long) && (m_ULong == other.m_ULong) && (m_String == other.m_String) && (m_LinearClass == other.m_LinearClass) && m_LinearStruct.Equals(other.m_LinearStruct);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals(obj as LinearClass);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
// Class containing a field of an unsupported primitive array type
internal class ClassWithUnsupportedPrimitiveArrayType
{
internal object[] m_InvalidArray = new object[1] { 123.4f }; // Only primitive type supported in object arrays is Int32
}
// Utility to create a GUID from a random number generator
static GUID CreateGuid(System.Random rnd)
{
string guidString = "";
for (int charNum = 0; charNum < 32; charNum++)
guidString = guidString + rnd.Next(10);
return new GUID(guidString);
}
// Utility to create a Hash128 from a random number generator
static Hash128 CreateHash128(System.Random rnd)
{
return new Hash128((uint)rnd.Next(), (uint)rnd.Next(), (uint)rnd.Next(), (uint)rnd.Next());
}
// Utility to create a CacheEntry instance from a random number generator
static CacheEntry CreateSyntheticCacheEntry(System.Random rnd)
{
CacheEntry cacheEntry = new CacheEntry();
cacheEntry.Hash = CreateHash128(rnd);
cacheEntry.Guid = CreateGuid(rnd);
cacheEntry.Version = rnd.Next(100);
cacheEntry.Type = (CacheEntry.EntryType)rnd.Next(4);
cacheEntry.File = "File_" + rnd.Next();
cacheEntry.ScriptType = "ScriptType_" + rnd.Next();
return cacheEntry;
}
// Utility to create an ObjectIdentifier instance from a random number generator
static ObjectIdentifier CreateObjectIdentifier(System.Random rnd)
{
ObjectIdentifier objectIdentifier = new ObjectIdentifier();
objectIdentifier.SetObjectIdentifier(CreateGuid(rnd), rnd.Next(), (FileType)rnd.Next(4), "FilePath_" + rnd.Next());
return objectIdentifier;
}
// Create a CachedInfo instance with every field populated from a given random number seed
// The data doesn't have to make sense, but everything has to have a value of some sort so we can check everything is being serialized/deserialized
static CachedInfo CreateSyntheticCachedInfo(int seed)
{
System.Random rnd = new System.Random(seed);
CachedInfo cachedData = new CachedInfo();
cachedData.Asset = CreateSyntheticCacheEntry(rnd);
cachedData.Dependencies = new CacheEntry[2];
cachedData.Dependencies[0] = CreateSyntheticCacheEntry(rnd);
cachedData.Dependencies[1] = CreateSyntheticCacheEntry(rnd);
cachedData.Data = new object[]
{
CreateSyntheticAssetLoadInfo(rnd),
CreateSyntheticBuildUsageTagSet(),
CreateSyntheticSpriteImporterData(rnd),
CreateSyntheticExtendedAssetData(rnd),
CreateSyntheticObjectTypes(rnd),
CreateSyntheticSceneDependencyInfo(rnd),
CreateHash128(rnd),
CreateSyntheticWriteResult(rnd),
CreateSyntheticSerializedFileMetaData(rnd),
CreateSyntheticBundleDetails(rnd)
};
return cachedData;
}
static AssetLoadInfo CreateSyntheticAssetLoadInfo(System.Random rnd)
{
AssetLoadInfo assetInfo = new AssetLoadInfo();
assetInfo.asset = CreateGuid(rnd);
assetInfo.address = "AnyAddress";
assetInfo.includedObjects = new List<ObjectIdentifier>();
assetInfo.includedObjects.Add(CreateObjectIdentifier(rnd));
assetInfo.includedObjects.Add(CreateObjectIdentifier(rnd));
assetInfo.referencedObjects = new List<ObjectIdentifier>();
assetInfo.referencedObjects.Add(CreateObjectIdentifier(rnd));
assetInfo.referencedObjects.Add(CreateObjectIdentifier(rnd));
return assetInfo;
}
static BuildUsageTagSet CreateSyntheticBuildUsageTagSet()
{
// JSON for a BuildUsageTagSet instance. There is no API to create this programatically so this was captured from a real project to be used in these tests
string buildUsageTagSetJsonData = "{\"m_objToUsage\":[{\"first\":{\"filePath\":\"\",\"fileType\":3,\"guid\":\"822642a2b47082c49966f0c54db535a4\",\"localIdentifierInFile\":-7728386467694932768},\"second\":{\"forceTextureReadable\":false,\"maxBonesPerVertex\":4,\"meshSupportedChannels\":12799,\"meshUsageFlags\":1,\"shaderIncludeInstancingVariants\":false,\"shaderUsageKeywordNames\":[],\"strippedPrefabObject\":false}},{\"first\":{\"filePath\":\"\",\"fileType\":3,\"guid\":\"90b695013db7b334cbcf925848031399\",\"localIdentifierInFile\":-1848259448780025149},\"second\":{\"forceTextureReadable\":false,\"maxBonesPerVertex\":0,\"meshSupportedChannels\":383,\"meshUsageFlags\":0,\"shaderIncludeInstancingVariants\":false,\"shaderUsageKeywordNames\":[],\"strippedPrefabObject\":false}},{\"first\":{\"filePath\":\"\",\"fileType\":3,\"guid\":\"db9aadf200fd84e4591cc30ea4d1358c\",\"localIdentifierInFile\":3883874861523733925},\"second\":{\"forceTextureReadable\":false,\"maxBonesPerVertex\":0,\"meshSupportedChannels\":383,\"meshUsageFlags\":0,\"shaderIncludeInstancingVariants\":false,\"shaderUsageKeywordNames\":[],\"strippedPrefabObject\":false}}]}";
BuildUsageTagSet buildUsageTagSet = new BuildUsageTagSet();
#if UNITY_2019_4_OR_NEWER
buildUsageTagSet.DeserializeFromJson(buildUsageTagSetJsonData);
#else
typeof(BuildUsageTagSet).GetMethod("DeserializeFromJson", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(buildUsageTagSet, new object[] { buildUsageTagSetJsonData });
#endif
return buildUsageTagSet;
}
static SpriteImporterData CreateSyntheticSpriteImporterData(System.Random rnd)
{
SpriteImporterData spriteImporterData = new SpriteImporterData();
spriteImporterData.PackedSprite = true;
spriteImporterData.SourceTexture = CreateObjectIdentifier(rnd);
return spriteImporterData;
}
static ExtendedAssetData CreateSyntheticExtendedAssetData(System.Random rnd)
{
ExtendedAssetData extendedAssetData = new ExtendedAssetData();
extendedAssetData.Representations = new List<ObjectIdentifier>();
extendedAssetData.Representations.Add(CreateObjectIdentifier(rnd));
extendedAssetData.Representations.Add(CreateObjectIdentifier(rnd));
return extendedAssetData;
}
static List<ObjectTypes> CreateSyntheticObjectTypes(System.Random rnd)
{
List<ObjectTypes> objectTypes = new List<ObjectTypes>();
objectTypes.Add(new ObjectTypes(CreateObjectIdentifier(rnd), new Type[] { typeof(int), typeof(CachedInfo) }));
objectTypes.Add(new ObjectTypes(CreateObjectIdentifier(rnd), new Type[] { typeof(MonoBehaviour), typeof(ScriptableObject) }));
return objectTypes;
}
#if !UNITY_2019_4_OR_NEWER
// Set the value of a named field on an instance using the reflection API. Required only for 2018.4 where we don't have direct access to internal fields
static void SetFieldValue<ObjectType>(ObjectType instance, string fieldName, object value)
{
typeof(ObjectType).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic).SetValue(instance, value);
}
#endif
// Create a populated 'SceneDependencyInfo' instance. Can set fields directly for 2019.4+ as internals are available but have to use reflection for 2018.4
static SceneDependencyInfo CreateSceneDependencyInfo(string scene, ObjectIdentifier[] objectIdentifiers, BuildUsageTagGlobal globalUsage, Type[] includedTypes)
{
SceneDependencyInfo sceneDependencyInfo = new SceneDependencyInfo();
#if UNITY_2019_4_OR_NEWER
sceneDependencyInfo.m_Scene = scene;
sceneDependencyInfo.m_ReferencedObjects = objectIdentifiers;
sceneDependencyInfo.m_GlobalUsage = globalUsage;
#if UNITY_2020_1_OR_NEWER
sceneDependencyInfo.m_IncludedTypes = includedTypes;
#endif
#else
sceneDependencyInfo.SetScene(scene);
sceneDependencyInfo.SetReferencedObjects(objectIdentifiers);
SetFieldValue(sceneDependencyInfo, "m_GlobalUsage", globalUsage);
#endif
return sceneDependencyInfo;
}
// Create a populated 'BuildUsageTagGlobal' instance. Can set fields directly for 2019.4+ as internals are available but have to use reflection for 2018.4
static BuildUsageTagGlobal CreateBuildUsageTagGlobal(uint lightmapModesUsed, uint legacyLightmapModesUsed, uint dynamicLightmapsUsed, uint fogModesUsed, bool forceInstancingStrip, bool forceInstancingKeep, bool shadowMasksUsed, bool subtractiveUsed, bool hybridRendererPackageUsed)
{
BuildUsageTagGlobal buildUsageTagGlobal = new BuildUsageTagGlobal();
#if UNITY_2019_4_OR_NEWER
buildUsageTagGlobal.m_LightmapModesUsed = lightmapModesUsed;
buildUsageTagGlobal.m_LegacyLightmapModesUsed = legacyLightmapModesUsed;
buildUsageTagGlobal.m_DynamicLightmapsUsed = dynamicLightmapsUsed;
buildUsageTagGlobal.m_FogModesUsed = fogModesUsed;
buildUsageTagGlobal.m_ForceInstancingStrip = forceInstancingStrip;
buildUsageTagGlobal.m_ForceInstancingKeep = forceInstancingKeep;
buildUsageTagGlobal.m_ShadowMasksUsed = shadowMasksUsed;
buildUsageTagGlobal.m_SubtractiveUsed = subtractiveUsed;
#if UNITY_2020_1_OR_NEWER
buildUsageTagGlobal.m_HybridRendererPackageUsed = hybridRendererPackageUsed;
#endif
#else
SetFieldValue(buildUsageTagGlobal, "m_LightmapModesUsed", lightmapModesUsed);
SetFieldValue(buildUsageTagGlobal, "m_LegacyLightmapModesUsed", legacyLightmapModesUsed);
SetFieldValue(buildUsageTagGlobal, "m_DynamicLightmapsUsed", dynamicLightmapsUsed);
SetFieldValue(buildUsageTagGlobal, "m_FogModesUsed", fogModesUsed);
SetFieldValue(buildUsageTagGlobal, "m_ForceInstancingStrip", forceInstancingStrip);
SetFieldValue(buildUsageTagGlobal, "m_ForceInstancingKeep", forceInstancingKeep);
SetFieldValue(buildUsageTagGlobal, "m_ShadowMasksUsed", shadowMasksUsed);
SetFieldValue(buildUsageTagGlobal, "m_SubtractiveUsed", subtractiveUsed);
#endif
return buildUsageTagGlobal;
}
static SceneDependencyInfo CreateSyntheticSceneDependencyInfo(System.Random rnd)
{
BuildUsageTagGlobal buildUsageTagGlobal = CreateBuildUsageTagGlobal((uint)rnd.Next(100), (uint)rnd.Next(100), (uint)rnd.Next(100), (uint)rnd.Next(100), ((rnd.Next() & 1) != 0), ((rnd.Next() & 1) != 0), ((rnd.Next() & 1) != 0), ((rnd.Next() & 1) != 0), ((rnd.Next() & 1) != 0));
return CreateSceneDependencyInfo("SceneName", new ObjectIdentifier[2] { CreateObjectIdentifier(rnd), CreateObjectIdentifier(rnd) }, buildUsageTagGlobal, new Type[] { typeof(MonoBehaviour), typeof(ScriptableObject) });
}
// Create a populated 'WriteResult' instance. Can set fields directly for 2019.4+ as internals are available but have to use reflection for 2018.4
static WriteResult CreateWriteResult(ObjectSerializedInfo[] serializedObjects, ResourceFile[] resourceFiles, Type[] includedTypes, String[] includedSerializeReferenceFQN)
{
WriteResult writeResult = new WriteResult();
#if UNITY_2019_4_OR_NEWER
writeResult.m_SerializedObjects = serializedObjects;
writeResult.m_ResourceFiles = resourceFiles;
writeResult.m_IncludedTypes = includedTypes;
writeResult.m_IncludedSerializeReferenceFQN = includedSerializeReferenceFQN;
#else
SetFieldValue(writeResult, "m_SerializedObjects", serializedObjects);
SetFieldValue(writeResult, "m_ResourceFiles", resourceFiles);
SetFieldValue(writeResult, "m_IncludedTypes", includedTypes);
#endif
return writeResult;
}
// Create a populated 'ObjectSerializedInfo' instance. Can set fields directly for 2019.4+ as internals are available but have to use reflection for 2018.4
static ObjectSerializedInfo CreateObjectSerializedInfo(ObjectIdentifier serializedObject, SerializedLocation header, SerializedLocation rawData)
{
ObjectSerializedInfo objectSerializedInfo = new ObjectSerializedInfo();
#if UNITY_2019_4_OR_NEWER
objectSerializedInfo.m_SerializedObject = serializedObject;
objectSerializedInfo.m_Header = header;
objectSerializedInfo.m_RawData = rawData;
#else
SetFieldValue(objectSerializedInfo, "m_SerializedObject", serializedObject);
SetFieldValue(objectSerializedInfo, "m_Header", header);
SetFieldValue(objectSerializedInfo, "m_RawData", rawData);
#endif
return objectSerializedInfo;
}
// Create a populated 'ResourceFile' instance. Can set fields directly for 2019.4+ as internals are available but have to use reflection for 2018.4
static ResourceFile CreateResourceFile(string filename, string fileAlias, bool serializedFile)
{
ResourceFile resourceFile = new ResourceFile();
#if UNITY_2019_4_OR_NEWER
resourceFile.m_FileName = filename;
resourceFile.m_FileAlias = fileAlias;
resourceFile.m_SerializedFile = serializedFile;
#else
SetFieldValue(resourceFile, "m_FileName", filename);
SetFieldValue(resourceFile, "m_FileAlias", fileAlias);
SetFieldValue(resourceFile, "m_SerializedFile", serializedFile);
#endif
return resourceFile;
}
// Create a populated 'SerializedLocation' instance. Can set fields directly for 2019.4+ as internals are available but have to use reflection for 2018.4
static SerializedLocation CreateSerializedLocation(string filename, uint offset, uint size)
{
SerializedLocation serializedLocation = new SerializedLocation();
#if UNITY_2019_4_OR_NEWER
serializedLocation.m_FileName = filename;
serializedLocation.m_Offset = offset;
serializedLocation.m_Size = size;
#else
SetFieldValue(serializedLocation, "m_FileName", filename);
SetFieldValue(serializedLocation, "m_Offset", offset);
SetFieldValue(serializedLocation, "m_Size", size);
#endif
return serializedLocation;
}
static WriteResult CreateSyntheticWriteResult(System.Random rnd)
{
ObjectSerializedInfo[] serializedObjects = new ObjectSerializedInfo[]
{
CreateObjectSerializedInfo(CreateObjectIdentifier(rnd), CreateSerializedLocation("Header_" + rnd.Next(), (uint)rnd.Next(), (uint)rnd.Next()), CreateSerializedLocation("RawData_" + rnd.Next(), (uint)rnd.Next(), (uint)rnd.Next())),
CreateObjectSerializedInfo(CreateObjectIdentifier(rnd), CreateSerializedLocation("Header_" + rnd.Next(), (uint)rnd.Next(), (uint)rnd.Next()), CreateSerializedLocation("RawData_" + rnd.Next(), (uint)rnd.Next(), (uint)rnd.Next()))
};
ResourceFile[] resourceFiles = new ResourceFile[]
{
CreateResourceFile("Filename_" + rnd.Next(), "FileAlias_" + rnd.Next(), ((rnd.Next() & 1) != 0)),
CreateResourceFile("Filename_" + rnd.Next(), "FileAlias_" + rnd.Next(), ((rnd.Next() & 1) != 0))
};
return CreateWriteResult(serializedObjects, resourceFiles, new Type[] { typeof(ScriptableObject), typeof(Vector2) }, new String[] { "IncludedSerializeReferenceFQN_" + rnd.Next(), "IncludedSerializeReferenceFQN_" + rnd.Next() });
}
static BundleDetails CreateSyntheticBundleDetails(System.Random rnd)
{
BundleDetails bundleDetails = new BundleDetails();
bundleDetails.FileName = "FileName_" + rnd.Next();
bundleDetails.Crc = (uint)rnd.Next();
bundleDetails.Hash = CreateHash128(rnd);
bundleDetails.Dependencies = new string[] { "Dependencies_" + rnd.Next(), "Dependencies_" + rnd.Next(), "Dependencies_" + rnd.Next() };
return bundleDetails;
}
static SerializedFileMetaData CreateSyntheticSerializedFileMetaData(System.Random rnd)
{
SerializedFileMetaData serializedFileMetaData = new SerializedFileMetaData();
serializedFileMetaData.RawFileHash = CreateHash128(rnd);
serializedFileMetaData.ContentHash = CreateHash128(rnd);
return serializedFileMetaData;
}
// Test some data serializes/deserializes correctly
// The 'data' instance is serialized to a MemoryStream then deserialized back again. The original and deserialized instances are then converted to text with the DumpToText utility class and the resultant text compared for equality
// The deserialized object is then compared directly to the original.
void TestSerializeData<DataType>(DataType data) where DataType : new()
{
TestSerializeData(data, null, null, null, true);
}
// Test some data serializes/deserializes correctly optionally using a collection of custom serializers, deserializers, object factories and custom object->text dumpers
// The 'data' instance is serialized to a MemoryStream then deserialized back again. The original and deserialized instances are then converted to text with the DumpToText utility class and the resultant text compared for equality
// If the 'testEquality' flag is true then the deserialized object is compared directly to the original. CachedInfo is the only type that doesn't do this as the full equality operation is not supported there
void TestSerializeData<DataType>(DataType data,
ICustomSerializer[] customSerializers,
(Type, DeSerializer.ObjectFactory)[] objectFactories,
DumpToText.ICustomDumper[] customDumpers,
bool testEquality) where DataType : new()
{
using (MemoryStream stream = new MemoryStream())
{
// Write to stream
Serializer serializer = new Serializer(customSerializers);
serializer.Serialize(stream, data, 1);
// Read from stream
stream.Position = 0;
DeSerializer deserializer = new DeSerializer(customSerializers, objectFactories);
DataType deserializedData = deserializer.DeSerialize<DataType>(stream);
// Dump initial object to text
DumpToText dumper = new DumpToText(customDumpers);
string originalText = dumper.Dump("Data", data).ToString();
#if DUMP_DATA_TEXT_TO_CONSOLE
Debug.Log("Original:");
Array.ForEach(originalText.Split('\n'), (textLine) => Debug.Log(textLine));
#endif
// Dump deserialized object to text
dumper.Clear();
string deserializedText = dumper.Dump("Data", deserializedData).ToString();
#if DUMP_DATA_TEXT_TO_CONSOLE
Debug.Log("Deserialized:");
Array.ForEach(deserializedText.Split('\n'), (textLine) => Debug.Log(textLine));
#endif
Assert.AreEqual(originalText, deserializedText);
if (testEquality)
Assert.AreEqual(data, deserializedData);
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8680029523237e1458574f6c8f1a3aa3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,20 @@
{
"name": "Unity.ScriptableBuildPipeline.Editor.Tests",
"references": [
"Unity.ScriptableBuildPipeline.Editor",
"Unity.ScriptableBuildPipeline",
"Unity.PerformanceTesting",
"Unity.PerformanceTesting.Editor"
],
"optionalUnityReferences": [
"TestAssemblies"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
]
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0244ca4987514734a84f74195d54113d
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,519 @@
using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
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.Build.Player;
using UnityEngine;
using UnityEngine.Rendering;
using static UnityEditor.Build.Pipeline.Utilities.BuildLog;
namespace UnityEditor.Build.Pipeline.Tests
{
public class WriteSerializedFileTests
{
class TestBuildParameters : TestBundleBuildParameters
{
public override bool UseCache { get; set; }
public override string TempOutputFolder { get; set; }
public override bool NonRecursiveDependencies { get; set; }
internal BuildSettings TestBuildSettings;
public override BuildSettings GetContentBuildSettings()
{
return TestBuildSettings;
}
}
class TestDependencyData : TestDependencyDataBase
{
public Dictionary<GUID, SceneDependencyInfo> TestSceneInfo = new Dictionary<GUID, SceneDependencyInfo>();
public override Dictionary<GUID, SceneDependencyInfo> SceneInfo => TestSceneInfo;
public override BuildUsageTagGlobal GlobalUsage => default(BuildUsageTagGlobal);
}
class TestWriteData : TestWriteDataBase
{
internal List<IWriteOperation> TestOps = new List<IWriteOperation>();
public override List<IWriteOperation> WriteOperations => TestOps;
}
class TestBuildResults : TestBuildResultsBase
{
Dictionary<string, WriteResult> m_Results = new Dictionary<string, WriteResult>();
Dictionary<string, SerializedFileMetaData> m_MetaData = new Dictionary<string, SerializedFileMetaData>();
public override Dictionary<string, WriteResult> WriteResults => m_Results;
public override Dictionary<string, SerializedFileMetaData> WriteResultsMetaData => m_MetaData;
}
class TestWriteOperation : IWriteOperation
{
internal int TestWriteCount;
public bool OutputSerializedFile = false;
public bool WriteBundle = false;
public WriteCommand TestCommand;
public WriteCommand Command { get => TestCommand; set => throw new System.NotImplementedException(); }
public BuildUsageTagSet UsageSet { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public BuildReferenceMap ReferenceMap { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public Hash128 DependencyHash { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
Hash128 debugHash = new Hash128();
public void SetDebugHash(uint hash)
{
debugHash = new Hash128(0, 0, 0, hash);
}
public Hash128 GetHash128(IBuildLogger log)
{
return HashingMethods.Calculate(debugHash, new Hash128(0, 0, 0, (uint)QualitySettingsApi.GetNumberOfLODsStripped())).ToHash128();
}
public Hash128 GetHash128()
{
return GetHash128(null);
}
internal static void WriteRandomData(Stream s, long size, int seed)
{
System.Random r = new System.Random(seed);
long written = 0;
byte[] bytes = new byte[Math.Min(1 * 1024 * 1024, size)];
while (written < size)
{
r.NextBytes(bytes);
int writeSize = (int)Math.Min(size - written, bytes.Length);
s.Write(bytes, 0, writeSize);
written += bytes.Length;
}
}
internal static void WriteRandomData(string filename, long size, int seed)
{
using (var s = File.Open(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
WriteRandomData(s, size, seed);
}
}
internal string CreateFileOfSize(string path, long size)
{
System.Random r = new System.Random(0);
Directory.CreateDirectory(Path.GetDirectoryName(path));
WriteRandomData(path, size, 0);
return path;
}
public WriteResult Write(string outputFolder, BuildSettings settings, BuildUsageTagGlobal globalUsage)
{
#if UNITY_2019_4_OR_NEWER
if (WriteBundle)
{
return ContentBuildInterface.WriteSerializedFile(outputFolder, new WriteParameters
{
writeCommand = new WriteCommand() { fileName = "bundle", internalName = "bundle" },
settings = settings,
globalUsage = globalUsage,
usageSet = new BuildUsageTagSet(),
referenceMap = new BuildReferenceMap(),
bundleInfo = new AssetBundleInfo() { bundleName = "bundle" }
});
}
#endif
string filename = Path.Combine(outputFolder, "resourceFilename");
CreateFileOfSize(filename, 1024);
TestWriteCount++;
WriteResult result = new WriteResult();
ResourceFile file = new ResourceFile();
file.SetFileName(filename);
file.SetSerializedFile(OutputSerializedFile);
result.SetResourceFiles(new ResourceFile[] { file });
if (OutputSerializedFile)
{
var obj1 = new ObjectSerializedInfo();
SerializedLocation header = new SerializedLocation();
header.SetFileName(result.resourceFiles[0].fileAlias);
header.SetOffset(100);
obj1.SetHeader(header);
var obj2 = new ObjectSerializedInfo();
SerializedLocation header2 = new SerializedLocation();
header2.SetFileName(result.resourceFiles[0].fileAlias);
header2.SetOffset(200);
obj2.SetHeader(header2);
result.SetSerializedObjects(new ObjectSerializedInfo[] { obj1, obj2 });
}
return result;
}
}
TestBuildParameters m_BuildParameters;
TestDependencyData m_DependencyData;
TestWriteData m_WriteData;
TestBuildResults m_BuildResults;
WriteSerializedFiles m_Task;
BuildCache m_Cache;
BuildContext m_Context;
BuildLog m_Log;
string m_TestTempDir;
bool m_PreviousSlimSettings;
bool m_PreviousStripUnusedMeshComponents;
bool m_PreviousBakeCollisionMeshes;
int m_PreviousQualityLevel;
List<int> m_PreviousMaximumLODLeve = new List<int>();
#if UNITY_2020_1_OR_NEWER
bool m_PreviousMipStripping;
List<int> m_PreviousMasterTextureLimits = new List<int>();
#endif
GraphicsDeviceType[] m_PreviousGraphicsAPIs;
bool m_PreviousUseDefaultGraphicsAPIs;
#if UNITY_EDITOR_WIN
// NOTE: Direct3D12 is marked as experimental, so use Direct3D11 for the after test
GraphicsDeviceType[] m_PlatformAPIsBefore = { GraphicsDeviceType.Direct3D12, GraphicsDeviceType.Direct3D11 };
GraphicsDeviceType[] m_PlatformAPIsAfter = { GraphicsDeviceType.Direct3D11 };
#elif UNITY_EDITOR_OSX
GraphicsDeviceType[] m_PlatformAPIsBefore = { GraphicsDeviceType.OpenGLCore, GraphicsDeviceType.Metal };
GraphicsDeviceType[] m_PlatformAPIsAfter = { GraphicsDeviceType.Metal };
#elif UNITY_EDITOR_LINUX
GraphicsDeviceType[] m_PlatformAPIsBefore = { GraphicsDeviceType.OpenGLCore, GraphicsDeviceType.Vulkan };
GraphicsDeviceType[] m_PlatformAPIsAfter = { GraphicsDeviceType.Vulkan };
#endif
[SetUp]
public void Setup()
{
m_PreviousSlimSettings = ScriptableBuildPipeline.slimWriteResults;
ScriptableBuildPipeline.s_Settings.slimWriteResults = false;
m_PreviousStripUnusedMeshComponents = PlayerSettings.stripUnusedMeshComponents;
PlayerSettings.stripUnusedMeshComponents = false;
m_PreviousBakeCollisionMeshes = PlayerSettings.bakeCollisionMeshes;
PlayerSettings.bakeCollisionMeshes = false;
QualitySettings_GetPrevious();
#if UNITY_2020_1_OR_NEWER
m_PreviousMipStripping = PlayerSettings.mipStripping;
PlayerSettings.mipStripping = false;
#endif
m_PreviousUseDefaultGraphicsAPIs = PlayerSettings.GetUseDefaultGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget);
PlayerSettings.SetUseDefaultGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget, false);
m_PreviousGraphicsAPIs = PlayerSettings.GetGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget);
PlayerSettings.SetGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget, m_PlatformAPIsBefore);
BuildCache.PurgeCache(false);
m_TestTempDir = Path.Combine("Temp", "test");
Directory.CreateDirectory(m_TestTempDir);
m_BuildParameters = new TestBuildParameters();
m_BuildParameters.UseCache = true;
m_BuildParameters.TempOutputFolder = m_TestTempDir;
m_BuildParameters.TestBuildSettings = new BuildSettings { target = EditorUserBuildSettings.activeBuildTarget };
m_DependencyData = new TestDependencyData();
m_WriteData = new TestWriteData();
m_WriteData.TestOps = new List<IWriteOperation>();
m_BuildResults = new TestBuildResults();
m_Task = new WriteSerializedFiles();
m_Cache = new BuildCache();
m_Log = new BuildLog();
m_Context = new BuildContext(m_BuildParameters, m_DependencyData, m_WriteData, m_BuildResults, m_Cache, m_Log);
ContextInjector.Inject(m_Context, m_Task);
}
[TearDown]
public void Teardown()
{
Directory.Delete(m_TestTempDir, true);
ScriptableBuildPipeline.s_Settings.slimWriteResults = m_PreviousSlimSettings;
PlayerSettings.stripUnusedMeshComponents = m_PreviousStripUnusedMeshComponents;
PlayerSettings.bakeCollisionMeshes = m_PreviousBakeCollisionMeshes;
QualitySettings_RestorePrevious();
#if UNITY_2020_1_OR_NEWER
PlayerSettings.mipStripping = m_PreviousMipStripping;
#endif
PlayerSettings.SetGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget, m_PreviousGraphicsAPIs);
PlayerSettings.SetUseDefaultGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget, m_PreviousUseDefaultGraphicsAPIs);
m_Cache.Dispose();
}
void QualitySettings_GetPrevious()
{
m_PreviousQualityLevel = QualitySettings.GetQualityLevel();
QualitySettings.SetQualityLevel(0);
#if UNITY_2020_1_OR_NEWER
m_PreviousMasterTextureLimits.Clear();
#endif
m_PreviousMaximumLODLeve.Clear();
var count = QualitySettings.names.Length;
for (int i = 0; i < count; i++)
{
#if UNITY_2020_1_OR_NEWER
m_PreviousMasterTextureLimits.Add(QualitySettings.masterTextureLimit);
QualitySettings.masterTextureLimit = 0;
#endif
m_PreviousMaximumLODLeve.Add(QualitySettings.maximumLODLevel);
QualitySettings.maximumLODLevel = 0;
QualitySettings.IncreaseLevel();
}
}
#if UNITY_2020_1_OR_NEWER
void SetQualitySettings_MasterTextureLimits(int limit)
{
QualitySettings.SetQualityLevel(0);
for (int i = 0; i < m_PreviousMasterTextureLimits.Count; i++)
{
QualitySettings.masterTextureLimit = limit;
QualitySettings.IncreaseLevel();
}
}
#endif
void SetQualitySettings_MaximumLODLevel(int level)
{
QualitySettings.SetQualityLevel(0);
for (int i = 0; i < m_PreviousMaximumLODLeve.Count; i++)
{
QualitySettings.maximumLODLevel = level;
QualitySettings.IncreaseLevel();
}
}
void QualitySettings_RestorePrevious()
{
QualitySettings.SetQualityLevel(0);
var count = QualitySettings.names.Length;
for (int i = 0; i < count; i++)
{
#if UNITY_2020_1_OR_NEWER
QualitySettings.masterTextureLimit = m_PreviousMasterTextureLimits[i];
#endif
QualitySettings.maximumLODLevel = m_PreviousMaximumLODLeve[i];
QualitySettings.IncreaseLevel();
}
QualitySettings.SetQualityLevel(m_PreviousQualityLevel);
}
TestWriteOperation AddTestOperation(string name = "testInternalName")
{
TestWriteOperation op = new TestWriteOperation();
op.TestCommand = new WriteCommand();
op.TestCommand.internalName = name;
m_WriteData.WriteOperations.Add(op);
return op;
}
public static IEnumerable RebuildTestCases
{
get
{
yield return new TestCaseData(false, new Action<WriteSerializedFileTests>((_this) => {})).SetName("NoChanges");
yield return new TestCaseData(true, new Action<WriteSerializedFileTests>((_this) => { _this.m_BuildParameters.TestBuildSettings.buildFlags |= ContentBuildFlags.DisableWriteTypeTree; })).SetName("BuildSettings");
yield return new TestCaseData(true, new Action<WriteSerializedFileTests>((_this) => { ((TestWriteOperation)_this.m_WriteData.WriteOperations[0]).SetDebugHash(27); })).SetName("OperationHash");
yield return new TestCaseData(true, new Action<WriteSerializedFileTests>((_this) => { ScriptableBuildPipeline.s_Settings.slimWriteResults = true; })).SetName("SlimWriteResults");
yield return new TestCaseData(true, new Action<WriteSerializedFileTests>((_this) => { PlayerSettings.stripUnusedMeshComponents = true; })).SetName("StripUnusedMeshComponents");
yield return new TestCaseData(true, new Action<WriteSerializedFileTests>((_this) => { PlayerSettings.bakeCollisionMeshes = true; })).SetName("BakeCollisionMeshes");
yield return new TestCaseData(true, new Action<WriteSerializedFileTests>((_this) => { _this.SetQualitySettings_MaximumLODLevel(1); })).SetName("LODStripping");
#if UNITY_2020_1_OR_NEWER
yield return new TestCaseData(true, new Action<WriteSerializedFileTests>((_this) => { _this.SetQualitySettings_MasterTextureLimits(1); PlayerSettings.mipStripping = true; })).SetName("MipStripping");
#endif
yield return new TestCaseData(true, new Action<WriteSerializedFileTests>((_this) => { PlayerSettings.SetGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget, _this.m_PlatformAPIsAfter); })).SetName("GraphicsAPIs");
}
}
[Test, TestCaseSource(typeof(WriteSerializedFileTests), "RebuildTestCases")]
public void WhenInputsChanges_OnlyChangedDependenciesTriggersRebuild(bool shouldRebuild, Action<WriteSerializedFileTests> postFirstBuildAction)
{
TestWriteOperation op = AddTestOperation();
m_Task.Run();
Assert.AreEqual(1, op.TestWriteCount);
m_Cache.SyncPendingSaves();
postFirstBuildAction(this);
m_BuildResults.WriteResults.Clear();
m_BuildResults.WriteResultsMetaData.Clear();
m_Task.Run();
Assert.AreEqual(shouldRebuild ? 2 : 1, op.TestWriteCount);
}
[Test]
public void WhenFileHasSerializedObjects_AndSlimMode_OnlyFirstObjectInWriteResults([Values] bool slimEnabled)
{
TestWriteOperation op = AddTestOperation();
op.OutputSerializedFile = true;
ScriptableBuildPipeline.s_Settings.slimWriteResults = slimEnabled;
m_Task.Run();
Assert.AreEqual(1, op.TestWriteCount);
WriteResult r = m_BuildResults.WriteResults[op.TestCommand.internalName];
if (slimEnabled)
{
Assert.AreEqual(1, r.serializedObjects.Count);
Assert.AreEqual(100, r.serializedObjects[0].header.offset);
}
else
{
Assert.AreEqual(2, r.serializedObjects.Count);
}
}
[Test]
public void WhenResourceFileIsNotASerializedFile_ContentHashIsFullFileHash()
{
TestWriteOperation op = AddTestOperation();
m_Task.Run();
Assert.AreEqual(1, op.TestWriteCount);
SerializedFileMetaData md = m_BuildResults.WriteResultsMetaData[op.TestCommand.internalName];
WriteResult result = m_BuildResults.WriteResults[op.TestCommand.internalName];
Assert.AreEqual(md.RawFileHash, md.ContentHash);
}
[Test]
public void WhenResourceFileIsASerializedFile_ContentHashBeginsAtFirstObject()
{
TestWriteOperation op = AddTestOperation();
op.OutputSerializedFile = true;
m_Task.Run();
Assert.AreEqual(1, op.TestWriteCount);
SerializedFileMetaData md = m_BuildResults.WriteResultsMetaData[op.TestCommand.internalName];
WriteResult result = m_BuildResults.WriteResults[op.TestCommand.internalName];
Hash128 expectedContentHash;
using (FileStream fs = File.OpenRead(result.resourceFiles[0].fileName))
{
fs.Position = (long)result.serializedObjects[0].header.offset;
expectedContentHash = HashingMethods.Calculate(new List<object>() { HashingMethods.CalculateStream(fs) }).ToHash128();
}
var objs = new List<object>() { HashingMethods.CalculateFile(result.resourceFiles[0].fileName) };
Hash128 fullFileHash = HashingMethods.Calculate(objs).ToHash128();
Assert.AreEqual(fullFileHash, md.RawFileHash);
Assert.AreEqual(expectedContentHash, md.ContentHash);
Assert.AreNotEqual(md.RawFileHash, md.ContentHash);
}
[Test]
public void WhenWriteResultsContainsMultipleSerializedFiles_ContentHashBeginsAtFirstObjectOfEachFile()
{
var resourceFiles = new ResourceFile[2];
resourceFiles[0].SetFileAlias("sf1");
resourceFiles[0].SetFileName($"{m_TestTempDir}/sf1");
resourceFiles[0].SetSerializedFile(true);
resourceFiles[1].SetFileAlias("sf2");
resourceFiles[1].SetFileName($"{m_TestTempDir}/sf2");
resourceFiles[1].SetSerializedFile(true);
var header1 = new SerializedLocation();
header1.SetFileName(resourceFiles[0].fileAlias);
header1.SetOffset(200);
var header2 = new SerializedLocation();
header2.SetFileName(resourceFiles[1].fileAlias);
header2.SetOffset(100);
var serializedObjects = new ObjectSerializedInfo[2];
serializedObjects[0].SetHeader(header1);
serializedObjects[1].SetHeader(header2);
WriteResult results = new WriteResult();
results.SetResourceFiles(resourceFiles);
results.SetSerializedObjects(serializedObjects);
// Create 2 files with bytes that are only different between the 2 m_Offsets
byte[] bytes = new byte[400];
for (int i = 0; i < 200; i++)
bytes[i] = 1;
for (int i = 200; i < 400; i++)
bytes[i] = 2;
File.WriteAllBytes(results.resourceFiles[0].fileName, bytes);
for (int i = 0; i < 100; i++)
bytes[i] = 1;
for (int i = 100; i < 200; i++)
bytes[i] = 3;
for (int i = 200; i < 400; i++)
bytes[i] = 2;
File.WriteAllBytes(results.resourceFiles[1].fileName, bytes);
var data1 = WriteSerializedFiles.CalculateFileMetadata(ref results);
// Now update the file bytes between the 2 m_Offsets
for (int i = 100; i < 200; i++)
bytes[i] = 4;
File.WriteAllBytes(results.resourceFiles[1].fileName, bytes);
var data2 = WriteSerializedFiles.CalculateFileMetadata(ref results);
Assert.AreNotEqual(data1.RawFileHash, data2.RawFileHash);
Assert.AreNotEqual(data1.ContentHash, data2.ContentHash);
}
[Test]
public void Run_CallsWriteOnOperationAndOutputsWriteResult()
{
TestWriteOperation op = AddTestOperation();
ReturnCode result = m_Task.Run();
Assert.AreEqual(1, op.TestWriteCount);
WriteResult reportedResult = m_BuildResults.WriteResults[op.Command.internalName];
FileAssert.Exists(reportedResult.resourceFiles[0].fileName);
}
[Test]
public void Run_WithoutCache_Succeeds()
{
m_BuildParameters.UseCache = false;
AddTestOperation("testOp1");
AddTestOperation("testOp2");
ReturnCode result = m_Task.Run();
Assert.AreEqual(ReturnCode.Success, result);
m_BuildParameters.UseCache = true;
}
#if UNITY_2020_2_OR_NEWER || ENABLE_DETAILED_PROFILE_CAPTURING
[Test]
public void WhenWritingSerializedFilesAndUsingDetailedBuildLog_ProfileCaptureScope_CreatesLogEventsWithinTaskThreshold()
{
TestWriteOperation op = AddTestOperation();
op.WriteBundle = true;
bool useDetailedBuildLog = ScriptableBuildPipeline.useDetailedBuildLog;
ScriptableBuildPipeline.useDetailedBuildLog = true;
m_Task.Run();
LogStep runCachedOp = m_Log.Root.Children.Find(x => x.Name == "RunCachedOperation");
LogStep processEntries = runCachedOp.Children.Find(x => x.Name == "Process Entries");
LogStep writingOp = processEntries.Children.Find(x => x.Name == "Writing TestWriteOperation");
Assert.IsTrue(writingOp.Children.Count > 0);
double taskEndTime = runCachedOp.StartTime + runCachedOp.DurationMS;
foreach(LogStep e in writingOp.Children)
{
Assert.LessOrEqual(e.StartTime + e.DurationMS, taskEndTime);
}
ScriptableBuildPipeline.useDetailedBuildLog = useDetailedBuildLog;
}
#endif
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a7a6cf901dabe64586a9bd99395189a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,124 @@
using NUnit.Framework;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEditor.Build.Pipeline.WriteTypes;
using UnityEngine;
namespace UnityEditor.Build.Pipeline.Tests
{
class WriteTypesTests
{
IWriteOperation[] WriteOperations;
[OneTimeSetUp]
public void OneTimeSetUp()
{
WriteOperations = new IWriteOperation[5];
WriteOperations[0] = new AssetBundleWriteOperation();
#pragma warning disable CS0618 // Type or member is obsolete
WriteOperations[1] = new RawWriteOperation();
#pragma warning restore CS0618 // Type or member is obsolete
WriteOperations[2] = new SceneBundleWriteOperation();
WriteOperations[3] = new SceneDataWriteOperation();
#pragma warning disable CS0618 // Type or member is obsolete
WriteOperations[4] = new SceneRawWriteOperation();
#pragma warning restore CS0618 // Type or member is obsolete
var command = new WriteCommand
{
fileName = GUID.Generate().ToString(),
internalName = GUID.Generate().ToString()
};
var usageSet = new BuildUsageTagSet();
var referenceMap = new BuildReferenceMap();
for (int i = 0; i < WriteOperations.Length; i++)
{
WriteOperations[i].Command = command;
WriteOperations[i].UsageSet = usageSet;
WriteOperations[i].ReferenceMap = referenceMap;
}
}
[OneTimeTearDown]
public void OnetimeTearDown()
{
WriteOperations = null;
}
[SetUp]
public void SetUp()
{
BuildInterfacesWrapper.SceneCallbackVersionHash = new Hash128();
BuildInterfacesWrapper.ShaderCallbackVersionHash = new Hash128();
}
[TearDown]
public void TearDown()
{
BuildInterfacesWrapper.SceneCallbackVersionHash = new Hash128();
BuildInterfacesWrapper.ShaderCallbackVersionHash = new Hash128();
}
[Test]
public void Changing_SceneCallbackVersionHash_ChangesHashesOf_SceneWriteOperations()
{
Hash128[] preHash = new Hash128[5];
for (int i = 0; i < WriteOperations.Length; i++)
preHash[i] = WriteOperations[i].GetHash128();
BuildInterfacesWrapper.SceneCallbackVersionHash = HashingMethods.Calculate(GUID.Generate()).ToHash128();
Hash128[] postHash = new Hash128[5];
for (int i = 0; i < WriteOperations.Length; i++)
postHash[i] = WriteOperations[i].GetHash128();
// We expect AssetBundleWriteOperation [0] and RawWriteOperation [1] to not be changed by the scene callback version hash.
for (int i = 0; i < 2; i++)
Assert.AreEqual(preHash[i], postHash[i], "{0} hash changed.", WriteOperations[i].GetType().Name);
// We expect SceneBundleWriteOperation [2], SceneDataWriteOperation [3], and SceneRawWriteOperation [4] to be changed by the scene callback version hash.
for (int i = 2; i < WriteOperations.Length; i++)
Assert.AreNotEqual(preHash[i], postHash[i], "{0} hash unchanged. Not", WriteOperations[i].GetType().Name);
}
[Test]
public void Changing_ShaderCallbackVersionHash_ChangesHashesOf_AllWriteOperations()
{
Hash128[] preHash = new Hash128[5];
for (int i = 0; i < WriteOperations.Length; i++)
preHash[i] = WriteOperations[i].GetHash128();
BuildInterfacesWrapper.ShaderCallbackVersionHash = HashingMethods.Calculate(GUID.Generate()).ToHash128();
Hash128[] postHash = new Hash128[5];
for (int i = 0; i < WriteOperations.Length; i++)
postHash[i] = WriteOperations[i].GetHash128();
// We expect all write operation hashes to be changed by the scene callback version hash.
for (int i = 0; i < WriteOperations.Length; i++)
Assert.AreNotEqual(preHash[i], postHash[i], "{0} hash unchanged.", WriteOperations[i].GetType().Name);
}
[Test]
public void Unchanged_CallbackVersionHashes_DoNotChangeHashesOf_AllWriteOperations()
{
Hash128[] preHash = new Hash128[5];
for (int i = 0; i < WriteOperations.Length; i++)
preHash[i] = WriteOperations[i].GetHash128();
// Just zero out the hash again
BuildInterfacesWrapper.SceneCallbackVersionHash = new Hash128();
BuildInterfacesWrapper.ShaderCallbackVersionHash = new Hash128();
Hash128[] postHash = new Hash128[5];
for (int i = 0; i < WriteOperations.Length; i++)
postHash[i] = WriteOperations[i].GetHash128();
// We expect no write operation hashes to be changed by the scene callback version hash.
for (int i = 0; i < WriteOperations.Length; i++)
Assert.AreEqual(preHash[i], postHash[i], "{0} hash changed.", WriteOperations[i].GetType().Name);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b92edd448d83cc4d8558e4e11ffc3bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,22 @@
{
"name": "Unity.ScriptableBuildPipeline.Tests",
"rootNamespace": "",
"references": [
"GUID:27619889b8ba8c24980f49ee34dbb44a",
"GUID:c5ecc461727906345a35491a0440694f",
"GUID:0acc523941302664db1f4e527237feb3"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: adbcba91892774149bea528141373ca2
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: