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,85 @@
using NUnit.Framework;
using Unity.Pipeline.Editor.Authoring;
using Unity.Pipeline.Editor.Commands.Assets;
using UnityEditor;
namespace Unity.Pipeline.Tests.Editor.Authoring
{
/// <summary>
/// Tests for the create_folder reference command (CLI-190), exercised directly and via
/// PipelineClient. Verifies the result envelope, recursive creation, Assets-relative paths,
/// and the traversal guard.
/// </summary>
public class FolderCommandsTests
{
private const string Root = "Assets/__CLI190Test";
[SetUp]
public void SetUp()
{
// Start from the default authoring root so a leftover EditorPref (from another test or an
// interrupted run) can't shift where bare paths resolve mid-suite.
ProjectPaths.ResetAuthoringRoot();
}
[TearDown]
public void TearDown()
{
ProjectPaths.ResetAuthoringRoot();
if (AssetDatabase.IsValidFolder(Root))
{
AssetDatabase.DeleteAsset(Root);
AssetDatabase.Refresh();
}
}
#region Direct
[Test]
public void CreateFolder_ExplicitAssetsPath_CreatesFoldersAndReturnsIdentity()
{
var result = FolderCommands.CreateFolder(Root + "/Sub/Leaf");
Assert.IsTrue(AssetDatabase.IsValidFolder(Root + "/Sub/Leaf"), "Nested folders should exist");
Assert.AreEqual(Root + "/Sub/Leaf", result.AssetPath);
Assert.IsNotEmpty(result.Guid, "Created folder should have a GUID");
}
[Test]
public void CreateFolder_BarePath_ResolvesUnderAssets()
{
// No "Assets/" prefix — should resolve under the default authoring root (Assets/).
var result = FolderCommands.CreateFolder("__CLI190Test/Bare");
Assert.AreEqual("Assets/__CLI190Test/Bare", result.AssetPath);
Assert.IsTrue(AssetDatabase.IsValidFolder("Assets/__CLI190Test/Bare"));
}
[Test]
public void CreateFolder_Traversal_Throws()
{
Assert.Throws<System.ArgumentException>(() => FolderCommands.CreateFolder("../Outside"));
Assert.Throws<System.ArgumentException>(() => FolderCommands.CreateFolder("Assets/../Outside"));
}
#endregion
#region ViaClient
[Test]
public void CreateFolder_ViaClient_Succeeds()
{
using (var server = new PipelineTestServer())
{
var response = server.Execute("create_folder", new { path = Root + "/ViaClient" });
Assert.IsTrue(response.IsSuccess, $"create_folder should succeed: {response.Error}");
Assert.IsTrue(response.HasValidJson, "Response should have valid JSON");
Assert.IsTrue(response.JsonResponse.ContainsKey("result"), "Should have result field");
Assert.IsTrue(AssetDatabase.IsValidFolder(Root + "/ViaClient"), "Folder should be created");
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4653539f2cf4a1b4699bd2c7a8d35188
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,223 @@
using NUnit.Framework;
using Newtonsoft.Json;
using Unity.Pipeline;
using Unity.Pipeline.Models;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Unity.Pipeline.Tests.Editor.Authoring
{
/// <summary>
/// Tests for <see cref="ObjectRefConverter"/>: a single canonical string handle coerces into the
/// matching ObjectRef field (mirroring ObjectResolver/ToString priority), structured objects still
/// deserialize fully, and serialization still emits a JSON object. Plus one end-to-end ViaClient
/// test proving a string target flows through the real /api/exec dispatch path.
/// </summary>
public class ObjectRefConverterTests
{
private static ObjectRef Parse(string handle) =>
JsonConvert.DeserializeObject<ObjectRef>("\"" + handle + "\"");
#region String coercion (one field per canonical form)
[Test]
public void String_HierarchyPath_LeadingSlash()
{
var r = Parse("/Player");
Assert.AreEqual("/Player", r.HierarchyPath);
Assert.IsNull(r.GlobalId); Assert.IsNull(r.Path); Assert.IsNull(r.Guid);
Assert.IsNull(r.InstanceId); Assert.IsNull(r.FileId);
}
[Test]
public void String_HierarchyPath_Nested()
{
Assert.AreEqual("/Level/Floor", Parse("/Level/Floor").HierarchyPath);
}
[Test]
public void String_GlobalId()
{
var r = Parse("GlobalObjectId_V1-2-abc123def4560000abc123def4560000-1095335325-0");
Assert.AreEqual("GlobalObjectId_V1-2-abc123def4560000abc123def4560000-1095335325-0", r.GlobalId);
Assert.IsNull(r.HierarchyPath);
}
[Test]
public void String_AssetPath()
{
var r = Parse("Assets/Prefabs/Enemy.prefab");
Assert.AreEqual("Assets/Prefabs/Enemy.prefab", r.Path);
Assert.IsNull(r.HierarchyPath);
}
[Test]
public void String_GuidWithFileId()
{
var r = Parse("guid:a1b2c3d4e5f6:123");
Assert.AreEqual("a1b2c3d4e5f6", r.Guid);
Assert.AreEqual(123L, r.FileId);
}
[Test]
public void String_GuidOnly_Prefixed()
{
var r = Parse("guid:a1b2c3d4e5f6");
Assert.AreEqual("a1b2c3d4e5f6", r.Guid);
Assert.IsNull(r.FileId);
}
[Test]
public void String_InstanceId_Prefixed_Negative()
{
#if !UNITY_6000_4_OR_NEWER
Assert.AreEqual(ObjectId.FromRaw(-3070), Parse("instanceId:-3070").InstanceId);
#else
Assert.Ignore("Negative int instance ids do not exist in the EntityId (ulong) model on 6000.4+.");
#endif
}
[Test]
public void String_PlainNegativeInt_IsInstanceId()
{
#if !UNITY_6000_4_OR_NEWER
var r = Parse("-3070");
Assert.AreEqual(ObjectId.FromRaw(-3070), r.InstanceId);
Assert.IsNull(r.HierarchyPath);
#else
Assert.Ignore("Negative int instance ids do not exist in the EntityId (ulong) model on 6000.4+.");
#endif
}
[Test]
public void String_PlainPositiveInt_IsInstanceId()
{
Assert.AreEqual(ObjectId.FromRaw(48184), Parse("48184").InstanceId);
}
[Test]
public void String_32Hex_IsGuid()
{
var r = Parse("a1b2c3d4e5f60718293a4b5c6d7e8f90");
Assert.AreEqual("a1b2c3d4e5f60718293a4b5c6d7e8f90", r.Guid);
Assert.IsNull(r.InstanceId);
}
[Test]
public void String_BareName_FallsBackToHierarchyPath()
{
var r = Parse("Enemy_1");
Assert.AreEqual("Enemy_1", r.HierarchyPath);
Assert.IsNull(r.InstanceId); Assert.IsNull(r.Guid);
}
#endregion
#region Authoring-root-relative asset paths (AUTHAPI-9)
[Test]
public void String_RelativePathWithExtension_IsPath()
{
// No "Assets/" prefix but a file extension: an authoring-root-relative asset path.
var r = Parse("Materials/FloorA.mat");
Assert.AreEqual("Materials/FloorA.mat", r.Path);
Assert.IsNull(r.HierarchyPath);
}
[Test]
public void String_BareFileWithExtension_IsPath()
{
var r = Parse("Enemy.prefab");
Assert.AreEqual("Enemy.prefab", r.Path);
Assert.IsNull(r.HierarchyPath);
}
[Test]
public void String_RelativeNoExtension_StaysHierarchyPath()
{
// No extension and no leading slash: still a scene hierarchy path, not an asset candidate.
var r = Parse("Root/Child");
Assert.AreEqual("Root/Child", r.HierarchyPath);
Assert.IsNull(r.Path);
}
[Test]
public void String_LeadingSlashWithExtension_StaysHierarchyPath()
{
// A leading slash marks a canonical hierarchy path even when a name looks dotted.
var r = Parse("/Root/Cube.001");
Assert.AreEqual("/Root/Cube.001", r.HierarchyPath);
Assert.IsNull(r.Path);
}
#endregion
#region Object form, write, null
[Test]
public void Object_Structured_DeserializesAllFields()
{
var json = "{\"globalId\":\"G\",\"path\":\"Assets/A\",\"guid\":\"a1\",\"fileId\":42,\"instanceId\":7,\"hierarchyPath\":\"/H\"}";
var r = JsonConvert.DeserializeObject<ObjectRef>(json);
Assert.AreEqual("G", r.GlobalId);
Assert.AreEqual("Assets/A", r.Path);
Assert.AreEqual("a1", r.Guid);
Assert.AreEqual(42L, r.FileId);
Assert.AreEqual(ObjectId.FromRaw(7), r.InstanceId);
Assert.AreEqual("/H", r.HierarchyPath);
}
[Test]
public void WriteJson_EmitsObject_NotString_AndRoundTrips()
{
var json = JsonConvert.SerializeObject(new ObjectRef { HierarchyPath = "/Player" });
StringAssert.StartsWith("{", json, "ObjectRef must serialize as a JSON object, not a bare string");
StringAssert.Contains("\"hierarchyPath\":\"/Player\"", json);
var back = JsonConvert.DeserializeObject<ObjectRef>(json);
Assert.AreEqual("/Player", back.HierarchyPath);
}
[Test]
public void Null_DeserializesToNull()
{
Assert.IsNull(JsonConvert.DeserializeObject<ObjectRef>("null"));
}
[Test]
public void Number_BareInteger_IsInstanceId()
{
Assert.AreEqual(ObjectId.FromRaw(48184), JsonConvert.DeserializeObject<ObjectRef>("48184").InstanceId);
}
#endregion
#region End-to-end via the real dispatch path
[Test]
public void SetTransform_StringTarget_ResolvesAndApplies()
{
var go = new GameObject("CLI_ObjRefDispatch");
try
{
using (var server = new PipelineTestServer())
{
var resp = server.Execute("set_transform",
new { target = "/CLI_ObjRefDispatch", position = new[] { 1f, 2f, 3f } });
Assert.IsTrue(resp.IsSuccess,
$"set_transform with a string target should succeed (string->ObjectRef coercion): {resp.Error}");
}
Assert.AreEqual(new Vector3(1f, 2f, 3f), go.transform.localPosition,
"The string handle should have resolved to the GameObject and applied the transform");
}
finally
{
Object.DestroyImmediate(go);
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b218af7c5c4799a4ab37d7a670a6256e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,145 @@
using NUnit.Framework;
using Unity.Pipeline.Editor.Authoring;
using Unity.Pipeline.Models;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
using Unity.Pipeline;
namespace Unity.Pipeline.Tests.Editor.Authoring
{
/// <summary>
/// Tests for the object-reference resolver foundation (CLI-190): each handle form resolves back
/// to the same object, and Describe produces a canonical identity.
/// </summary>
public class ObjectResolverTests
{
private const string AssetFolder = "Assets/AUTHAPI9_Res";
private GameObject m_SceneObject;
[TearDown]
public void TearDown()
{
if (m_SceneObject != null)
Object.DestroyImmediate(m_SceneObject);
m_SceneObject = null;
if (AssetDatabase.IsValidFolder(AssetFolder))
{
AssetDatabase.DeleteAsset(AssetFolder);
AssetDatabase.Refresh();
}
}
private static string CreateMaterialAsset(string name)
{
if (!AssetDatabase.IsValidFolder(AssetFolder))
AssetDatabase.CreateFolder("Assets", "AUTHAPI9_Res");
var shader = Shader.Find("Standard")
?? Shader.Find("Universal Render Pipeline/Lit")
?? Shader.Find("Sprites/Default");
var mat = new Material(shader);
var path = $"{AssetFolder}/{name}.mat";
AssetDatabase.CreateAsset(mat, path);
AssetDatabase.SaveAssets();
return path;
}
[Test]
public void Describe_SceneObject_ProducesInstanceIdAndHierarchyPath()
{
m_SceneObject = new GameObject("CLI190_Root");
var child = new GameObject("Child");
child.transform.SetParent(m_SceneObject.transform);
var info = ObjectResolver.Describe(child);
Assert.IsNotNull(info);
Assert.AreEqual(PipelineUtils.GetObjectId(child), info.InstanceId);
Assert.AreEqual("/CLI190_Root/Child", info.HierarchyPath);
Assert.AreEqual("GameObject", info.Type);
Assert.IsNull(info.AssetPath, "A scene object should not report an asset path");
}
[Test]
public void Resolve_ByInstanceId_ReturnsSameObject()
{
m_SceneObject = new GameObject("CLI190_ById");
var handle = new ObjectRef { InstanceId = PipelineUtils.GetObjectId(m_SceneObject) };
Assert.IsTrue(ObjectResolver.TryResolve(handle, out var obj, out var error), error);
Assert.AreSame(m_SceneObject, obj);
}
[Test]
public void Resolve_ByHierarchyPath_ReturnsSameObject()
{
m_SceneObject = new GameObject("CLI190_ByPath");
var handle = new ObjectRef { HierarchyPath = "/CLI190_ByPath" };
Assert.IsTrue(ObjectResolver.TryResolve(handle, out var obj, out var error), error);
Assert.AreSame(m_SceneObject, obj);
}
[Test]
public void Resolve_EmptyHandle_FailsWithError()
{
Assert.IsFalse(ObjectResolver.TryResolve(new ObjectRef(), out _, out var error));
Assert.IsNotEmpty(error);
}
[Test]
public void Resolve_UnknownGuid_FailsWithError()
{
var handle = new ObjectRef { Guid = "00000000000000000000000000000000" };
Assert.IsFalse(ObjectResolver.TryResolve(handle, out _, out var error));
Assert.IsNotEmpty(error);
}
[Test]
public void Resolve_ByRelativeAssetPath_NormalizesUnderAuthoringRoot()
{
// A path without the "Assets/" prefix (AUTHAPI-9) resolves under the authoring root.
var full = CreateMaterialAsset("Floor");
var expected = AssetDatabase.LoadMainAssetAtPath(full);
var handle = new ObjectRef { Path = "AUTHAPI9_Res/Floor.mat" };
Assert.IsTrue(ObjectResolver.TryResolve(handle, out var obj, out var error), error);
Assert.AreSame(expected, obj);
}
[Test]
public void Resolve_ExplicitAssetsPath_StillResolves()
{
var full = CreateMaterialAsset("Wall");
var handle = new ObjectRef { Path = full };
Assert.IsTrue(ObjectResolver.TryResolve(handle, out var obj, out var error), error);
Assert.AreSame(AssetDatabase.LoadMainAssetAtPath(full), obj);
}
[Test]
public void Resolve_DottedGameObjectName_FallsBackToHierarchy()
{
// A dotted name (e.g. "Cube.001") is routed to Path by the string converter but is really a
// scene object; the Path branch falls back to a hierarchy lookup.
m_SceneObject = new GameObject("Cube.001");
var handle = new ObjectRef { Path = "Cube.001" };
Assert.IsTrue(ObjectResolver.TryResolve(handle, out var obj, out var error), error);
Assert.AreSame(m_SceneObject, obj);
}
[Test]
public void Resolve_UnresolvableRelativePath_ErrorListsEveryStrategy()
{
var handle = new ObjectRef { Path = "AUTHAPI9_Res/Missing.mat" };
Assert.IsFalse(ObjectResolver.TryResolve(handle, out _, out var error));
StringAssert.Contains("no asset at", error);
StringAssert.Contains("no GameObject at hierarchy path", error);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e864b00059a57364d902089021b33742
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,84 @@
using NUnit.Framework;
using Unity.Pipeline.Editor.Authoring;
namespace Unity.Pipeline.Tests.Editor.Authoring
{
/// <summary>
/// Tests for ProjectPaths (CLI-190): Assets-relative resolution, the traversal/out-of-root
/// guards, and the configurable authoring root.
/// </summary>
public class ProjectPathsTests
{
[SetUp]
public void SetUp()
{
// Reset to the default root before each test so a leftover EditorPref can't leak in.
ProjectPaths.ResetAuthoringRoot();
}
[TearDown]
public void TearDown()
{
ProjectPaths.ResetAuthoringRoot();
}
[Test]
public void DefaultRoot_IsAssets()
{
ProjectPaths.ResetAuthoringRoot();
Assert.AreEqual("Assets", ProjectPaths.AuthoringRoot);
}
[Test]
public void Resolve_BarePath_PrependsAssets()
{
var resolved = ProjectPaths.Resolve("Gameplay/Enemies", out var error);
Assert.IsNull(error, error);
Assert.AreEqual("Assets/Gameplay/Enemies", resolved);
}
[Test]
public void Resolve_ExplicitAssetsPath_Unchanged()
{
var resolved = ProjectPaths.Resolve("Assets/Gameplay", out var error);
Assert.IsNull(error, error);
Assert.AreEqual("Assets/Gameplay", resolved);
}
[Test]
public void Resolve_Traversal_Fails()
{
Assert.IsNull(ProjectPaths.Resolve("../Outside", out var error));
Assert.IsNotEmpty(error);
}
[Test]
public void Resolve_EmptyPath_Fails()
{
Assert.IsNull(ProjectPaths.Resolve("", out var error));
Assert.IsNotEmpty(error);
}
[Test]
public void ConfigurableRoot_ConfinesBareAndRejectsOutside()
{
ProjectPaths.AuthoringRoot = "Assets/__CLI190Root";
// Bare path resolves under the configured root.
var inside = ProjectPaths.Resolve("Foo/Bar", out var insideError);
Assert.IsNull(insideError, insideError);
Assert.AreEqual("Assets/__CLI190Root/Foo/Bar", inside);
// Explicit path outside the configured root is rejected (confinement).
Assert.IsNull(ProjectPaths.Resolve("Assets/Other", out var outsideError));
Assert.IsNotEmpty(outsideError);
}
[Test]
public void SetAuthoringRoot_OutsideAssets_Throws()
{
Assert.Throws<System.ArgumentException>(() => ProjectPaths.AuthoringRoot = "Outside");
Assert.Throws<System.ArgumentException>(() => ProjectPaths.AuthoringRoot = "Assets/../Escape");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9af4698d059fd9d43b72c6e1dad56424
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: