using System.IO; using Newtonsoft.Json.Linq; using NUnit.Framework; using Unity.Pipeline.Editor.Authoring; using Unity.Pipeline.Editor.Commands.Assets; using Unity.Pipeline.Models; using UnityEditor; using UnityEngine; namespace Unity.Pipeline.Tests.Editor.Assets { /// /// Tests for the CLI-212 per-type import settings + platform overrides: /// get_import_settings and the platform-aware extension of set_import_settings. /// /// Real importers are exercised by writing tiny generated assets, so the actual Unity read/write /// code paths (including per-platform overrides) are covered end to end: a 4x4 PNG (TextureImporter), /// a minimal OBJ (ModelImporter), and a tiny 8-bit PCM WAV (AudioImporter). The audio fixture drives /// a full per-platform override round-trip (ApplyAudioPlatform + /// ReadAudioPlatformOverride): set override, read it back, then clear it. /// public class ImportSettingsCommandTests { private const string Root = "Assets/__CLI212ImportTest"; [TearDown] public void TearDown() { ProjectPaths.ResetAuthoringRoot(); if (AssetDatabase.IsValidFolder(Root)) { AssetDatabase.DeleteAsset(Root); AssetDatabase.Refresh(); } } private static ObjectRef PathRef(string path) => new ObjectRef { Path = path }; // ------------------------------------------------------------------ asset fixtures /// Write a tiny 4x4 PNG so Unity imports it with a TextureImporter. private static string CreateTexture(string name = "Tex.png") { EnsureRoot(); var tex = new Texture2D(4, 4, TextureFormat.RGBA32, false); var pixels = new Color32[16]; for (var i = 0; i < pixels.Length; i++) pixels[i] = new Color32(255, 128, 64, 255); tex.SetPixels32(pixels); tex.Apply(); var bytes = tex.EncodeToPNG(); Object.DestroyImmediate(tex); var projectPath = Root + "/" + name; File.WriteAllBytes(ToAbsolute(projectPath), bytes); AssetDatabase.ImportAsset(projectPath, ImportAssetOptions.ForceSynchronousImport); return projectPath; } /// Write a minimal single-triangle OBJ so Unity imports it with a ModelImporter. private static string CreateModel(string name = "Tri.obj") { EnsureRoot(); const string obj = "o Tri\n" + "v 0 0 0\n" + "v 1 0 0\n" + "v 0 1 0\n" + "vn 0 0 1\n" + "f 1//1 2//1 3//1\n"; var projectPath = Root + "/" + name; File.WriteAllText(ToAbsolute(projectPath), obj); AssetDatabase.ImportAsset(projectPath, ImportAssetOptions.ForceSynchronousImport); return projectPath; } /// /// Write a tiny mono 8-bit PCM WAV (a handful of samples) so Unity imports it with an /// AudioImporter — enough to exercise the real per-platform override read/write paths. /// private static string CreateAudio(string name = "Beep.wav") { EnsureRoot(); const int sampleRate = 8000; const int sampleCount = 64; var pcm = new byte[sampleCount]; for (var i = 0; i < pcm.Length; i++) pcm[i] = (byte)(128 + (i % 2 == 0 ? 32 : -32)); // crude square wave around the 8-bit midpoint using (var stream = new MemoryStream()) using (var w = new BinaryWriter(stream)) { const short channels = 1; const short bitsPerSample = 8; var byteRate = sampleRate * channels * bitsPerSample / 8; var blockAlign = (short)(channels * bitsPerSample / 8); // RIFF header w.Write(new[] { 'R', 'I', 'F', 'F' }); w.Write(36 + pcm.Length); w.Write(new[] { 'W', 'A', 'V', 'E' }); // fmt chunk w.Write(new[] { 'f', 'm', 't', ' ' }); w.Write(16); // PCM fmt chunk size w.Write((short)1); // audio format = PCM w.Write(channels); w.Write(sampleRate); w.Write(byteRate); w.Write(blockAlign); w.Write(bitsPerSample); // data chunk w.Write(new[] { 'd', 'a', 't', 'a' }); w.Write(pcm.Length); w.Write(pcm); w.Flush(); var projectPath = Root + "/" + name; File.WriteAllBytes(ToAbsolute(projectPath), stream.ToArray()); AssetDatabase.ImportAsset(projectPath, ImportAssetOptions.ForceSynchronousImport); return projectPath; } } private static void EnsureRoot() { if (!AssetDatabase.IsValidFolder(Root)) AssetDatabase.CreateFolder("Assets", "__CLI212ImportTest"); } private static string ToAbsolute(string projectRelative) => $"{ProjectPaths.ProjectRoot.Replace('\\', '/')}/{projectRelative}"; // ------------------------------------------------------------------ texture: read [Test] public void GetImportSettings_Texture_ReturnsStructuredDefaultsAndOverrideBlock() { var path = CreateTexture(); var result = AssetImportCommands.GetImportSettings(PathRef(path), platform: "Android"); Assert.AreEqual("TextureImporter", result.ImporterType); Assert.AreEqual("Android", result.Platform); Assert.IsNotNull(result.Settings); Assert.IsTrue(result.Settings.ContainsKey("textureType")); Assert.IsTrue(result.Settings.ContainsKey("isReadable")); Assert.IsTrue(result.Settings.ContainsKey("mipmapEnabled")); Assert.IsTrue(result.Settings.ContainsKey("wrapMode")); Assert.IsNotNull(result.PlatformOverride, "Texture should expose a platform override block"); Assert.IsTrue(result.PlatformOverride.ContainsKey("overridden")); } // ------------------------------------------------------------------ texture: default round-trip [Test] public void SetImportSettings_TextureDefault_AppliesAndReadBackReflects() { var path = CreateTexture(); var settings = new JObject { ["isReadable"] = true, ["textureType"] = "NormalMap" }; var set = AssetImportCommands.SetImportSettings(PathRef(path), settings); CollectionAssert.Contains(set.Applied, "isReadable"); CollectionAssert.Contains(set.Applied, "textureType"); CollectionAssert.IsEmpty(set.Unknown); var read = AssetImportCommands.GetImportSettings(PathRef(path)); Assert.AreEqual(true, read.Settings["isReadable"]); Assert.AreEqual("NormalMap", read.Settings["textureType"]); } // ------------------------------------------------------------------ texture: platform override set + clear [Test] public void SetImportSettings_TextureAndroidOverride_SetsThenClears() { var path = CreateTexture(); // Default platform maxTextureSize before the override (should be unchanged afterwards). var defaultBefore = AssetImportCommands.GetImportSettings(PathRef(path)); var defaultMaxBefore = defaultBefore.Settings["maxTextureSize"]; var set = AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["maxTextureSize"] = 1024, ["format"] = "ASTC_6x6", ["overridden"] = true }, platform: "Android"); CollectionAssert.Contains(set.Applied, "maxTextureSize"); CollectionAssert.Contains(set.Applied, "format"); var afterSet = AssetImportCommands.GetImportSettings(PathRef(path), platform: "Android"); Assert.AreEqual(true, afterSet.PlatformOverride["overridden"]); Assert.AreEqual(1024, afterSet.PlatformOverride["maxTextureSize"]); Assert.AreEqual("ASTC_6x6", afterSet.PlatformOverride["format"]); // Default platform unchanged by the Android override. var defaultAfter = AssetImportCommands.GetImportSettings(PathRef(path)); Assert.AreEqual(defaultMaxBefore, defaultAfter.Settings["maxTextureSize"], "Default platform settings must be unchanged by a platform override"); // Clear the override. AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["overridden"] = false }, platform: "Android"); var afterClear = AssetImportCommands.GetImportSettings(PathRef(path), platform: "Android"); Assert.AreEqual(false, afterClear.PlatformOverride["overridden"], "Override should be cleared"); } // ------------------------------------------------------------------ audio: read [Test] public void GetImportSettings_Audio_ReturnsStructuredDefaultsAndOverrideBlock() { var path = CreateAudio(); var result = AssetImportCommands.GetImportSettings(PathRef(path), platform: "Android"); Assert.AreEqual("AudioImporter", result.ImporterType); Assert.AreEqual("Android", result.Platform); Assert.IsNotNull(result.Settings); Assert.IsTrue(result.Settings.ContainsKey("loadType")); Assert.IsTrue(result.Settings.ContainsKey("compressionFormat")); Assert.IsTrue(result.Settings.ContainsKey("quality")); Assert.IsNotNull(result.PlatformOverride, "Audio should expose a platform override block"); Assert.IsTrue(result.PlatformOverride.ContainsKey("overridden")); // No override has been written yet. Assert.AreEqual(false, result.PlatformOverride["overridden"]); } // ------------------------------------------------------------------ audio: platform override set + clear [Test] public void SetImportSettings_AudioAndroidOverride_SetsThenClears() { var path = CreateAudio(); // Default-platform compressionFormat before the override (must be unchanged afterwards). var defaultBefore = AssetImportCommands.GetImportSettings(PathRef(path)); var defaultCompressionBefore = defaultBefore.Settings["compressionFormat"]; var set = AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["compressionFormat"] = "Vorbis", ["loadType"] = "CompressedInMemory", ["overridden"] = true }, platform: "Android"); CollectionAssert.Contains(set.Applied, "compressionFormat"); CollectionAssert.Contains(set.Applied, "loadType"); CollectionAssert.Contains(set.Applied, "overridden"); CollectionAssert.IsEmpty(set.Unknown); var afterSet = AssetImportCommands.GetImportSettings(PathRef(path), platform: "Android"); Assert.AreEqual(true, afterSet.PlatformOverride["overridden"], "Override should be present after a platform write"); Assert.AreEqual("Vorbis", afterSet.PlatformOverride["compressionFormat"]); Assert.AreEqual("CompressedInMemory", afterSet.PlatformOverride["loadType"]); // Default platform unchanged by the Android override. var defaultAfter = AssetImportCommands.GetImportSettings(PathRef(path)); Assert.AreEqual(defaultCompressionBefore, defaultAfter.Settings["compressionFormat"], "Default platform settings must be unchanged by a platform override"); // Clear the override. AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["overridden"] = false }, platform: "Android"); var afterClear = AssetImportCommands.GetImportSettings(PathRef(path), platform: "Android"); Assert.AreEqual(false, afterClear.PlatformOverride["overridden"], "Override should be cleared"); } // ------------------------------------------------------------------ model: read + default write [Test] public void GetImportSettings_Model_ReturnsNullPlatformOverride() { var path = CreateModel(); var result = AssetImportCommands.GetImportSettings(PathRef(path)); Assert.AreEqual("ModelImporter", result.ImporterType); Assert.IsTrue(result.Settings.ContainsKey("meshCompression")); Assert.IsTrue(result.Settings.ContainsKey("animationType")); Assert.IsTrue(result.Settings.ContainsKey("materialImportMode")); Assert.IsNull(result.PlatformOverride, "ModelImporter has no per-platform overrides"); } [Test] public void SetImportSettings_ModelDefault_AppliesAndReadBack() { var path = CreateModel(); var set = AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["meshCompression"] = "High", ["importNormals"] = "Calculate" }); CollectionAssert.Contains(set.Applied, "meshCompression"); CollectionAssert.Contains(set.Applied, "importNormals"); var read = AssetImportCommands.GetImportSettings(PathRef(path)); Assert.AreEqual("High", read.Settings["meshCompression"]); Assert.AreEqual("Calculate", read.Settings["importNormals"]); } [Test] public void SetImportSettings_ModelWithPlatform_ReturnsNoPlatformOverrides_AndWritesNothing() { var path = CreateModel(); var before = AssetImportCommands.GetImportSettings(PathRef(path)); var meshBefore = before.Settings["meshCompression"]; var ex = Assert.Throws(() => AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["meshCompression"] = "High" }, platform: "iOS")); StringAssert.Contains("no_platform_overrides", ex.Message); var after = AssetImportCommands.GetImportSettings(PathRef(path)); Assert.AreEqual(meshBefore, after.Settings["meshCompression"], "A rejected platform write must change nothing"); } // ------------------------------------------------------------------ dry_run [Test] public void SetImportSettings_DryRun_ReportsButDoesNotChange() { var path = CreateTexture(); var before = AssetImportCommands.GetImportSettings(PathRef(path)); var readableBefore = (bool)before.Settings["isReadable"]; var result = AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["isReadable"] = !readableBefore }, dryRun: true); CollectionAssert.Contains(result.Applied, "isReadable"); var after = AssetImportCommands.GetImportSettings(PathRef(path)); Assert.AreEqual(readableBefore, (bool)after.Settings["isReadable"], "dry_run must not change settings"); } // ------------------------------------------------------------------ unknown key handling [Test] public void SetImportSettings_UnknownKey_GoesToUnknown() { var path = CreateTexture(); var result = AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["isReadable"] = true, ["definitelyNotAMember"] = 42 }); CollectionAssert.Contains(result.Applied, "isReadable"); CollectionAssert.Contains(result.Unknown, "definitelyNotAMember"); } [Test] public void SetImportSettings_AllUnknown_Throws() { var path = CreateTexture(); Assert.Throws(() => AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["definitelyNotAMember"] = 42 })); } // ------------------------------------------------------------------ platform validation [Test] public void SetImportSettings_UnknownPlatform_Throws() { var path = CreateTexture(); var ex = Assert.Throws(() => AssetImportCommands.SetImportSettings( PathRef(path), new JObject { ["maxTextureSize"] = 512 }, platform: "PlayStation")); StringAssert.Contains("unknown_platform", ex.Message); } [Test] public void GetImportSettings_UnknownPlatform_Throws() { var path = CreateTexture(); Assert.Throws(() => AssetImportCommands.GetImportSettings(PathRef(path), platform: "PlayStation")); } // ------------------------------------------------------------------ sandbox guard [Test] public void GetImportSettings_OutsideAuthoringRoot_Throws() { var path = CreateTexture(); // Confine the authoring root to a sub-folder that does NOT contain the asset, so the asset's // resolved path falls outside the sandbox. ProjectPaths.AuthoringRoot = Root + "/Nested"; Assert.Throws(() => AssetImportCommands.GetImportSettings(PathRef(path))); } // ------------------------------------------------------------------ via client (wire) smoke test [Test] public void GetImportSettings_ViaClient_Succeeds() { var path = CreateTexture(); using (var server = new PipelineTestServer()) { var response = server.Execute("get_import_settings", new { asset = new { path }, platform = "Android" }); Assert.IsTrue(response.IsSuccess, $"get_import_settings should succeed: {response.Error}"); Assert.IsTrue(response.HasValidJson, "Response should have valid JSON"); Assert.IsTrue(response.JsonResponse.ContainsKey("result"), "Should have result field"); } } } }