Files
AIC-Project/Client/PackageLocal/com.unity.pipeline@0.4.0-exp.1/Tests/Editor/Materials/MaterialCommandsTests.cs
T

375 lines
16 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
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";
}
/// <summary>The Color property to exercise: URP/Lit uses _BaseColor; built-in Standard uses _Color.</summary>
private string ColorProp => m_HasUrpLit ? "_BaseColor" : "_Color";
private static void EnsureFolder(string folder)
{
if (!AssetDatabase.IsValidFolder(folder))
AssetDatabase.CreateFolder("Assets", Path.GetFileName(folder));
}
/// <summary>Create a .mat asset with the given shader and return a path-based handle to it.</summary>
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 };
}
/// <summary>Create a tiny 2x2 texture asset on disk and return a handle to it.</summary>
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);
/// <summary>
/// Extract the float components of a Color/Vector property value. The direct (in-process) path
/// returns a <c>float[]</c>; tolerate any IEnumerable (e.g. a JArray) so the helper also works
/// if the value was JSON round-tripped.
/// </summary>
private static float[] Components(object value)
{
Assert.IsNotNull(value, "property value should not be null");
return ((System.Collections.IEnumerable)value)
.Cast<object>()
.Select(o => System.Convert.ToSingle(o is JToken t ? (object)t.ToObject<float>() : 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<Texture>(tex.Path);
var mat = AssetDatabase.LoadAssetAtPath<Material>(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<Material>(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<System.ArgumentException>(() =>
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<ShaderNotFoundException>(() =>
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);
}
}
}
}