Add Unity Pipeline package support

This commit is contained in:
ud18010
2026-07-31 17:34:04 +08:00
parent d1a526094e
commit 786b7ffff9
590 changed files with 51995 additions and 2 deletions
@@ -0,0 +1,374 @@
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);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a96b5f4084674e609bbfbb12e73bde08
@@ -0,0 +1,177 @@
using System.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 shader discovery / introspection commands (list_shaders /
/// get_shader_properties). URP-specific assertions are guarded with Assert.Ignore so the suite runs
/// on a Built-in RP project too.
/// </summary>
public class ShaderCommandsTests
{
private bool m_HasUrpLit;
[SetUp]
public void SetUp()
{
ProjectPaths.ResetAuthoringRoot();
m_HasUrpLit = Shader.Find("Universal Render Pipeline/Lit") != null;
}
[TearDown]
public void TearDown() => ProjectPaths.ResetAuthoringRoot();
// ---- list_shaders -------------------------------------------------------------------------
[Test]
public void ListShaders_FilterLit_ReturnsTheLitShader()
{
var results = ShaderCommands.ListShaders(filter: "Lit");
Assert.IsNotEmpty(results, "filtering by 'Lit' should return at least one shader");
Assert.IsTrue(results.All(s => s.Name.IndexOf("Lit", System.StringComparison.OrdinalIgnoreCase) >= 0),
"every result should contain the filter substring");
// Only assert the specific shader name when URP/Lit is present; this branch never runs
// otherwise, so a plain constant keeps the intent clear.
if (m_HasUrpLit)
Assert.IsTrue(results.Any(s => s.Name == "Universal Render Pipeline/Lit"),
"the active pipeline's Lit shader should be listed (AC #10)");
}
[Test]
public void ListShaders_ReportsIsSupportedAndName()
{
var results = ShaderCommands.ListShaders(filter: "Lit", limit: 50);
if (results.Count == 0)
Assert.Ignore("No 'Lit' shaders in this project.");
foreach (var s in results)
Assert.IsFalse(string.IsNullOrEmpty(s.Name), "every shader entry must carry a name");
// isSupported is a bool that always serializes; just assert the field is meaningful for a known shader.
var known = results.FirstOrDefault();
Assert.IsNotNull(known);
}
[Test]
public void ListShaders_RespectsLimit()
{
var results = ShaderCommands.ListShaders(limit: 3);
Assert.LessOrEqual(results.Count, 3, "limit should cap the result count");
}
[Test]
public void ListShaders_ExcludeBuiltin_DropsEngineShaders()
{
var withBuiltin = ShaderCommands.ListShaders(includeBuiltin: true);
var withoutBuiltin = ShaderCommands.ListShaders(includeBuiltin: false);
Assert.IsFalse(withoutBuiltin.Any(s => s.IsBuiltin),
"includeBuiltin=false must not return any built-in shaders");
Assert.GreaterOrEqual(withBuiltin.Count, withoutBuiltin.Count);
}
// ---- get_shader_properties ----------------------------------------------------------------
[Test]
public void GetShaderProperties_Urp_ReturnsBaseColorMetallicBaseMap()
{
if (!m_HasUrpLit)
Assert.Ignore("URP/Lit not available; skipping URP property introspection (AC #11).");
var result = ShaderCommands.GetShaderProperties(shader: "Universal Render Pipeline/Lit");
Assert.AreEqual("Universal Render Pipeline/Lit", result.Shader);
var baseColor = result.Properties.FirstOrDefault(p => p.Name == "_BaseColor");
Assert.IsNotNull(baseColor, "_BaseColor should be present");
Assert.AreEqual("Color", baseColor.Type);
var metallic = result.Properties.FirstOrDefault(p => p.Name == "_Metallic");
Assert.IsNotNull(metallic, "_Metallic should be present");
Assert.AreEqual("Range", metallic.Type);
Assert.IsNotNull(metallic.Range, "_Metallic should carry min/max");
var baseMap = result.Properties.FirstOrDefault(p => p.Name == "_BaseMap");
Assert.IsNotNull(baseMap, "_BaseMap should be present");
Assert.AreEqual("TexEnv", baseMap.Type);
Assert.AreEqual("Tex2D", baseMap.TextureDimension);
}
[Test]
public void GetShaderProperties_KnownShader_ReturnsProperties()
{
// Shader-agnostic: any real shader exposes a non-empty property list with names + types.
var shaderName = m_HasUrpLit ? "Universal Render Pipeline/Lit"
: (Shader.Find("Standard") != null ? "Standard" : "Sprites/Default");
var result = ShaderCommands.GetShaderProperties(shader: shaderName);
Assert.AreEqual(shaderName, result.Shader);
Assert.IsNotEmpty(result.Properties);
foreach (var p in result.Properties)
{
Assert.IsFalse(string.IsNullOrEmpty(p.Name));
Assert.IsFalse(string.IsNullOrEmpty(p.Type));
Assert.IsNotNull(p.Flags, "flags should never be null (empty list when None)");
}
}
[Test]
public void GetShaderProperties_FromMaterial_ReadsShaderOffMaterial()
{
var shaderName = Shader.Find("Standard") != null ? "Standard"
: (m_HasUrpLit ? "Universal Render Pipeline/Lit" : "Sprites/Default");
var folder = "Assets/__CLI213ShaderTest";
if (!AssetDatabase.IsValidFolder(folder))
AssetDatabase.CreateFolder("Assets", "__CLI213ShaderTest");
try
{
var path = $"{folder}/Mat.mat";
AssetDatabase.CreateAsset(new Material(Shader.Find(shaderName)), path);
AssetDatabase.SaveAssets();
var result = ShaderCommands.GetShaderProperties(material: new ObjectRef { Path = path });
Assert.AreEqual(shaderName, result.Shader);
Assert.IsNotEmpty(result.Properties);
}
finally
{
AssetDatabase.DeleteAsset(folder);
AssetDatabase.Refresh();
}
}
[Test]
public void GetShaderProperties_NotFound_Throws()
{
var ex = Assert.Throws<ShaderNotFoundException>(() =>
ShaderCommands.GetShaderProperties(shader: "No/Such/Shader__CLI213"));
StringAssert.Contains("shader_not_found", ex.Message);
}
[Test]
public void GetShaderProperties_NeitherArg_Throws()
{
Assert.Throws<System.ArgumentException>(() => ShaderCommands.GetShaderProperties());
}
// ---- ViaClient ----------------------------------------------------------------------------
[Test]
public void ListShaders_ViaClient_Succeeds()
{
using (var server = new PipelineTestServer())
{
var response = server.Execute("list_shaders", new { filter = "Lit", limit = 20 });
Assert.IsTrue(response.IsSuccess, $"list_shaders should succeed: {response.Error}");
Assert.IsTrue(response.HasValidJson, "response should be valid JSON");
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cd771dc30e5f4fbe9611c004593a09c4