using System.IO; using System.Linq; using Newtonsoft.Json.Linq; using NUnit.Framework; using Unity.Pipeline.Editor.Authoring; using Unity.Pipeline.Editor.Commands.Materials; using Unity.Pipeline.Models; using UnityEditor; using UnityEngine; namespace Unity.Pipeline.Tests.Editor.Materials { /// /// Tests for the CLI-213 material authoring commands (get_material_properties / /// set_material_properties). All assets are generated in-test under a temp folder and torn down /// afterwards. URP-specific assertions (e.g. _BaseColor / _Metallic Range / _BaseMap) are guarded /// so the suite still runs (with Assert.Ignore) on a Built-in RP project that lacks URP/Lit. /// public class MaterialCommandsTests { private const string Root = "Assets/__CLI213MaterialTest"; private string m_KnownShader; // a shader guaranteed to resolve on this project private bool m_HasUrpLit; [SetUp] public void SetUp() { ProjectPaths.ResetAuthoringRoot(); m_HasUrpLit = Shader.Find("Universal Render Pipeline/Lit") != null; m_KnownShader = PickKnownShader(); EnsureFolder(Root); } [TearDown] public void TearDown() { ProjectPaths.ResetAuthoringRoot(); if (AssetDatabase.IsValidFolder(Root)) { AssetDatabase.DeleteAsset(Root); AssetDatabase.Refresh(); } } private static string PickKnownShader() { if (Shader.Find("Standard") != null) return "Standard"; if (Shader.Find("Universal Render Pipeline/Lit") != null) return "Universal Render Pipeline/Lit"; return "Sprites/Default"; } /// The Color property to exercise: URP/Lit uses _BaseColor; built-in Standard uses _Color. private string ColorProp => m_HasUrpLit ? "_BaseColor" : "_Color"; private static void EnsureFolder(string folder) { if (!AssetDatabase.IsValidFolder(folder)) AssetDatabase.CreateFolder("Assets", Path.GetFileName(folder)); } /// Create a .mat asset with the given shader and return a path-based handle to it. private ObjectRef CreateMaterial(string name, string shaderName = null) { var shader = Shader.Find(shaderName ?? m_KnownShader); Assert.IsNotNull(shader, $"Test prerequisite: shader '{shaderName ?? m_KnownShader}' must resolve"); var mat = new Material(shader); var path = $"{Root}/{name}.mat"; AssetDatabase.CreateAsset(mat, path); AssetDatabase.SaveAssets(); return new ObjectRef { Path = path }; } /// Create a tiny 2x2 texture asset on disk and return a handle to it. private ObjectRef CreateTexture(string name) { var tex = new Texture2D(2, 2); tex.SetPixels(new[] { Color.white, Color.white, Color.white, Color.white }); tex.Apply(); var bytes = tex.EncodeToPNG(); Object.DestroyImmediate(tex); var path = $"{Root}/{name}.png"; File.WriteAllBytes(Path.Combine(ProjectPaths.ProjectRoot, path), bytes); AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); return new ObjectRef { Path = path }; } private static MaterialPropertyValue Prop(MaterialPropertiesResult r, string name) => r.Properties.FirstOrDefault(p => p.Name == name); /// /// Extract the float components of a Color/Vector property value. The direct (in-process) path /// returns a float[]; tolerate any IEnumerable (e.g. a JArray) so the helper also works /// if the value was JSON round-tripped. /// private static float[] Components(object value) { Assert.IsNotNull(value, "property value should not be null"); return ((System.Collections.IEnumerable)value) .Cast() .Select(o => System.Convert.ToSingle(o is JToken t ? (object)t.ToObject() : o)) .ToArray(); } // ---- get_material_properties -------------------------------------------------------------- [Test] public void Get_ReturnsShaderRenderQueueKeywordsAndProperties() { var handle = CreateMaterial("Get"); var result = MaterialCommands.GetMaterialProperties(handle); Assert.AreEqual(Shader.Find(m_KnownShader).name, result.Shader); Assert.IsNotNull(result.EnabledKeywords); Assert.IsNotEmpty(result.Properties, "A real shader should expose at least one property"); var color = Prop(result, ColorProp); Assert.IsNotNull(color, $"{ColorProp} should be present"); Assert.AreEqual("Color", color.Type); } [Test] public void Get_Urp_BaseColorIsColor_MetallicIsRangeWithLimits() { if (!m_HasUrpLit) Assert.Ignore("URP/Lit not available in this project; skipping URP-specific assertion (AC #1)."); var handle = CreateMaterial("UrpGet", "Universal Render Pipeline/Lit"); var result = MaterialCommands.GetMaterialProperties(handle); var baseColor = Prop(result, "_BaseColor"); Assert.IsNotNull(baseColor, "_BaseColor should be present"); Assert.AreEqual("Color", baseColor.Type); var metallic = Prop(result, "_Metallic"); Assert.IsNotNull(metallic, "_Metallic should be present"); Assert.AreEqual("Range", metallic.Type); Assert.IsNotNull(metallic.Range, "_Metallic (Range) should carry range limits"); Assert.LessOrEqual(metallic.Range.Min, metallic.Range.Max); } // ---- set_material_properties: color ------------------------------------------------------- [Test] public void Set_BaseColor_ViaArray_AppliesRed() { var handle = CreateMaterial("SetColorArray"); var props = new JObject { [ColorProp] = new JArray(1, 0, 0, 1) }; var result = MaterialCommands.SetMaterialProperties(handle, properties: props); CollectionAssert.Contains(result.Applied, ColorProp); var read = MaterialCommands.GetMaterialProperties(handle); var c = Components(Prop(read, ColorProp).Value); Assert.AreEqual(1f, c[0], 1e-4f); Assert.AreEqual(0f, c[1], 1e-4f); Assert.AreEqual(0f, c[2], 1e-4f); Assert.AreEqual(1f, c[3], 1e-4f); } [Test] public void Set_BaseColor_ViaHex_AppliesGreen() { var handle = CreateMaterial("SetColorHex"); var props = new JObject { [ColorProp] = "#00FF00" }; MaterialCommands.SetMaterialProperties(handle, properties: props); var read = MaterialCommands.GetMaterialProperties(handle); var c = Components(Prop(read, ColorProp).Value); Assert.AreEqual(0f, c[0], 1e-3f); Assert.AreEqual(1f, c[1], 1e-3f); Assert.AreEqual(0f, c[2], 1e-3f); Assert.AreEqual(1f, c[3], 1e-3f, "hex without alpha should default alpha to 1.0"); } // ---- set_material_properties: texture ----------------------------------------------------- [Test] public void Set_Texture_AssignsThenClears() { if (!m_HasUrpLit) Assert.Ignore("URP/Lit not available; skipping _BaseMap texture test (AC #4)."); var handle = CreateMaterial("SetTex", "Universal Render Pipeline/Lit"); var tex = CreateTexture("Albedo"); var set = MaterialCommands.SetMaterialProperties(handle, properties: new JObject { ["_BaseMap"] = JToken.FromObject(tex) }); CollectionAssert.Contains(set.Applied, "_BaseMap"); var loadedTex = AssetDatabase.LoadAssetAtPath(tex.Path); var mat = AssetDatabase.LoadAssetAtPath(handle.Path); Assert.AreEqual(loadedTex, mat.GetTexture("_BaseMap"), "texture should be assigned"); // Clear with null. MaterialCommands.SetMaterialProperties(handle, properties: new JObject { ["_BaseMap"] = JValue.CreateNull() }); mat = AssetDatabase.LoadAssetAtPath(handle.Path); Assert.IsNull(mat.GetTexture("_BaseMap"), "passing null should clear the texture"); } [Test] public void Set_Texture_OutsideAuthoringRoot_IsRejected() { if (!m_HasUrpLit) Assert.Ignore("URP/Lit not available; skipping _BaseMap sandbox test (AC #13)."); // Confine the authoring root to a subfolder, then reference a texture created OUTSIDE it. var outsideTex = CreateTexture("OutsideAlbedo"); // lives directly under Root var sub = $"{Root}/Sub"; if (!AssetDatabase.IsValidFolder(sub)) AssetDatabase.CreateFolder(Root, "Sub"); var handle = CreateMaterial("Sub/SetTexSandbox", "Universal Render Pipeline/Lit"); ProjectPaths.AuthoringRoot = sub; // now Root/OutsideAlbedo.png is outside the root var ex = Assert.Throws(() => MaterialCommands.SetMaterialProperties(handle, properties: new JObject { ["_BaseMap"] = JToken.FromObject(outsideTex) })); StringAssert.Contains("outside the authoring root", ex.Message); } // ---- shader reassignment ------------------------------------------------------------------ [Test] public void Set_Shader_Reassigns() { if (Shader.Find("Standard") == null) Assert.Ignore("Built-in Standard shader not available; skipping shader-reassign test (AC #5)."); var handle = CreateMaterial("Reassign", m_KnownShader); var result = MaterialCommands.SetMaterialProperties(handle, shader: "Standard"); Assert.AreEqual("Standard", result.Shader); var read = MaterialCommands.GetMaterialProperties(handle); Assert.AreEqual("Standard", read.Shader); } [Test] public void Set_Shader_NotFound_ThrowsAndWritesNothing() { var handle = CreateMaterial("BadShader"); var before = MaterialCommands.GetMaterialProperties(handle).Shader; var ex = Assert.Throws(() => MaterialCommands.SetMaterialProperties(handle, shader: "No/Such/Shader__CLI213", properties: new JObject { [ColorProp] = new JArray(1, 0, 0, 1) })); StringAssert.Contains("shader_not_found", ex.Message); Assert.AreEqual("shader_not_found", ex.Code); var after = MaterialCommands.GetMaterialProperties(handle).Shader; Assert.AreEqual(before, after, "a shader_not_found must write nothing"); } // ---- keywords ----------------------------------------------------------------------------- [Test] public void Set_Keyword_EnableThenDisable() { var handle = CreateMaterial("Keyword"); const string keyword = "_EMISSION"; MaterialCommands.SetMaterialProperties(handle, enableKeywords: new[] { keyword }); var enabled = MaterialCommands.GetMaterialProperties(handle).EnabledKeywords; CollectionAssert.Contains(enabled, keyword); MaterialCommands.SetMaterialProperties(handle, disableKeywords: new[] { keyword }); var disabled = MaterialCommands.GetMaterialProperties(handle).EnabledKeywords; CollectionAssert.DoesNotContain(disabled, keyword); } // ---- render queue ------------------------------------------------------------------------- [Test] public void Set_RenderQueue_ExplicitThenInherit() { var handle = CreateMaterial("Queue"); MaterialCommands.SetMaterialProperties(handle, renderQueue: 3000); Assert.AreEqual(3000, MaterialCommands.GetMaterialProperties(handle).RenderQueue, "an explicit renderQueue must be reflected by the command's own read-back"); MaterialCommands.SetMaterialProperties(handle, renderQueue: -1); // Validate inheritance via the command's raw queue read rather than the effective queue: the shader // default can legitimately equal the prior override (e.g. 3000), so comparing the effective // value would be flaky. The command returns -1 exactly when the material inherits the shader's. Assert.AreEqual(-1, MaterialCommands.GetMaterialProperties(handle).RenderQueue, "renderQueue -1 should clear the override and inherit from the shader"); } // ---- unknown / mismatch ------------------------------------------------------------------- [Test] public void Set_UnknownProperty_GoesToUnknown_StillAppliesOthers() { var handle = CreateMaterial("Unknown"); var props = new JObject { [ColorProp] = new JArray(0, 0, 1, 1), ["_DefinitelyNotARealProp_CLI213"] = 5, }; var result = MaterialCommands.SetMaterialProperties(handle, properties: props); CollectionAssert.Contains(result.Applied, ColorProp); Assert.IsTrue(result.Unknown.Any(u => u.Contains("_DefinitelyNotARealProp_CLI213")), "unknown property should be reported in unknown[]"); } [Test] public void Set_TypeMismatch_GoesToUnknownWithReason() { if (!m_HasUrpLit) Assert.Ignore("URP/Lit not available; skipping _Metallic type-mismatch test."); var handle = CreateMaterial("Mismatch", "Universal Render Pipeline/Lit"); // _Metallic is a Range (number); send an array to force a mismatch. var props = new JObject { ["_BaseColor"] = new JArray(1, 1, 1, 1), ["_Metallic"] = new JArray(1, 2, 3), }; var result = MaterialCommands.SetMaterialProperties(handle, properties: props); CollectionAssert.Contains(result.Applied, "_BaseColor"); Assert.IsTrue(result.Unknown.Any(u => u.Contains("_Metallic")), "a type mismatch should be reported in unknown[] with a reason"); } // ---- dry_run ------------------------------------------------------------------------------ [Test] public void Set_DryRun_ReportsButDoesNotWrite() { var handle = CreateMaterial("DryRun"); var beforeColor = Components(Prop(MaterialCommands.GetMaterialProperties(handle), ColorProp).Value); var props = new JObject { [ColorProp] = new JArray(1, 0, 0, 1) }; var result = MaterialCommands.SetMaterialProperties(handle, properties: props, dryRun: true); CollectionAssert.Contains(result.Applied, ColorProp); var afterColor = Components(Prop(MaterialCommands.GetMaterialProperties(handle), ColorProp).Value); CollectionAssert.AreEqual(beforeColor, afterColor, "dry_run must not change the material"); } // ---- ViaClient ---------------------------------------------------------------------------- [Test] public void Set_ViaClient_AppliesColor() { var handle = CreateMaterial("ViaClient"); using (var server = new PipelineTestServer()) { var response = server.Execute("set_material_properties", new { material = new { path = handle.Path }, properties = new JObject { [ColorProp] = new JArray(1, 0, 0, 1) }, }); Assert.IsTrue(response.IsSuccess, $"set_material_properties should succeed: {response.Error}"); var read = MaterialCommands.GetMaterialProperties(handle); var c = Components(Prop(read, ColorProp).Value); Assert.AreEqual(1f, c[0], 1e-4f); } } } }