Add Unity Pipeline package support
This commit is contained in:
+377
@@ -0,0 +1,377 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Unity.Pipeline.Editor.Commands.GameObjects;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
using Unity.Pipeline;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Editor.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the shared string-encoded JSON parameter coercion (CLI-219 / CLI-220) and the batch
|
||||
/// <c>create_gameobjects</c> command (CLI-223).
|
||||
///
|
||||
/// CLI-219/220 are a single root cause: when an agent/CLI passes a structured parameter as a
|
||||
/// JSON-ENCODED STRING (position "[1,2,3]", properties "{\"m_Mass\":0.17}"), the server must
|
||||
/// re-parse it before converting. The ViaClient tests below pass those forms as real strings to
|
||||
/// prove the coercion; the Direct tests pass real typed values to prove the typed path still works.
|
||||
/// The guard is also pinned: a plain string param and an ObjectRef string handle ("/Name") must NOT
|
||||
/// be re-parsed.
|
||||
/// </summary>
|
||||
public class CoercionAndBatchTests
|
||||
{
|
||||
private readonly List<GameObject> m_Spawned = new List<GameObject>();
|
||||
|
||||
private GameObject Track(GameObject go)
|
||||
{
|
||||
m_Spawned.Add(go);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static ObjectRef RefTo(Object obj) => new ObjectRef { InstanceId = PipelineUtils.GetObjectId(obj) };
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var go in m_Spawned)
|
||||
{
|
||||
if (go != null)
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
m_Spawned.Clear();
|
||||
}
|
||||
|
||||
#region CLI-219 set_transform
|
||||
|
||||
[Test]
|
||||
public void SetTransform_Direct_TypedArrays_Apply()
|
||||
{
|
||||
var go = Track(new GameObject("Xform219_Direct"));
|
||||
|
||||
GameObjectCommands.SetTransform(RefTo(go),
|
||||
position: new[] { 1f, 2f, 3f },
|
||||
rotation: new[] { 0f, 90f, 0f },
|
||||
scale: new[] { 2f, 2f, 2f });
|
||||
|
||||
Assert.AreEqual(new Vector3(1, 2, 3), go.transform.localPosition);
|
||||
Assert.AreEqual(new Vector3(2, 2, 2), go.transform.localScale);
|
||||
Assert.AreEqual(90f, go.transform.localEulerAngles.y, 0.01f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetTransform_ViaClient_ArrayParams_Apply()
|
||||
{
|
||||
var go = Track(new GameObject("Xform219_Arrays"));
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("set_transform", new
|
||||
{
|
||||
target = new { instanceId = PipelineUtils.GetObjectId(go) },
|
||||
position = new[] { 1f, 2f, 3f },
|
||||
rotation = new[] { 0f, 90f, 0f },
|
||||
scale = new[] { 2f, 2f, 2f }
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"set_transform should succeed: {response.Error}");
|
||||
Assert.AreEqual(new Vector3(1, 2, 3), go.transform.localPosition);
|
||||
Assert.AreEqual(new Vector3(2, 2, 2), go.transform.localScale);
|
||||
Assert.AreEqual(90f, go.transform.localEulerAngles.y, 0.01f);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetTransform_ViaClient_JsonStringParams_Coerced()
|
||||
{
|
||||
// The CLI-219 repro: position/rotation/scale arrive as JSON-encoded STRINGS. Before the
|
||||
// coercion these convert to null (JValue(string).ToObject(float[]) == null) and the command
|
||||
// silently applies nothing. They must now be re-parsed and applied.
|
||||
var go = Track(new GameObject("Xform219_Strings"));
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("set_transform", new
|
||||
{
|
||||
target = new { instanceId = PipelineUtils.GetObjectId(go) },
|
||||
position = "[1,2,3]",
|
||||
rotation = "[0,90,0]",
|
||||
scale = "[2,2,2]"
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"set_transform should succeed: {response.Error}");
|
||||
Assert.AreEqual(new Vector3(1, 2, 3), go.transform.localPosition,
|
||||
"String-encoded position must be coerced and applied");
|
||||
Assert.AreEqual(new Vector3(2, 2, 2), go.transform.localScale,
|
||||
"String-encoded scale must be coerced and applied");
|
||||
Assert.AreEqual(90f, go.transform.localEulerAngles.y, 0.01f,
|
||||
"String-encoded rotation must be coerced and applied");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetTransform_ViaClient_NestedTarget_StringParams_Coerced()
|
||||
{
|
||||
// Cover a parented/nested target too: the handle resolves the child, and the string-encoded
|
||||
// position applies as a LOCAL transform on the child.
|
||||
var parent = Track(new GameObject("Xform219_Parent"));
|
||||
var child = Track(new GameObject("Xform219_Child"));
|
||||
child.transform.SetParent(parent.transform);
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("set_transform", new
|
||||
{
|
||||
target = new { instanceId = PipelineUtils.GetObjectId(child) },
|
||||
position = "[4,5,6]"
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"set_transform should succeed: {response.Error}");
|
||||
Assert.AreEqual(parent.transform, child.transform.parent, "Child stays parented");
|
||||
Assert.AreEqual(new Vector3(4, 5, 6), child.transform.localPosition,
|
||||
"String-encoded local position must apply to the nested child");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CLI-220 set_component_properties
|
||||
|
||||
[Test]
|
||||
public void SetComponentProperties_Direct_JObject_Applies()
|
||||
{
|
||||
var go = Track(new GameObject("Props220_Direct"));
|
||||
var rb = go.AddComponent<Rigidbody>();
|
||||
|
||||
ComponentCommands.SetComponentProperties(RefTo(rb), new JObject { ["m_Mass"] = 0.17f });
|
||||
|
||||
Assert.AreEqual(0.17f, rb.mass, 0.0001f, "Real JObject properties must still apply");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetComponentProperties_ViaClient_PropertiesAsJsonString_Coerced()
|
||||
{
|
||||
// The CLI-220 repro: `properties` arrives as a JSON-encoded STRING. Before the coercion it
|
||||
// converts to null and the command fails "Required parameter 'properties' is missing or
|
||||
// empty". It must now be re-parsed into a JObject and applied.
|
||||
var go = Track(new GameObject("Props220_String"));
|
||||
var rb = go.AddComponent<Rigidbody>();
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("set_component_properties", new
|
||||
{
|
||||
target = new { instanceId = PipelineUtils.GetObjectId(rb) },
|
||||
type = "Rigidbody",
|
||||
properties = "{\"m_Mass\":0.17}"
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess,
|
||||
$"set_component_properties should succeed with a string-encoded properties: {response.Error}");
|
||||
Assert.AreEqual(0.17f, rb.mass, 0.0001f,
|
||||
"String-encoded properties must be coerced and applied");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CLI-223 create_gameobjects (batch)
|
||||
|
||||
[Test]
|
||||
public void CreateGameObjects_Direct_CountOne_ReturnsSingleIdentity()
|
||||
{
|
||||
// Regression: count=1 keeps the bare name (no "1" suffix) and returns exactly one item.
|
||||
var result = GameObjectCommands.CreateGameObjects(name: "Batch223_One", primitive: "cube", count: 1);
|
||||
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual(1, result.GameObjects.Count);
|
||||
|
||||
var go = PipelineUtils.IdToObject(result.GameObjects[0].InstanceId.Value) as GameObject;
|
||||
Track(go);
|
||||
Assert.IsNotNull(go, "Created object should be resolvable by instanceId");
|
||||
Assert.AreEqual("Batch223_One", go.name, "count=1 must not append a numeric suffix");
|
||||
Assert.IsNotNull(go.GetComponent<BoxCollider>(), "Cube primitive should have a BoxCollider");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateGameObjects_Direct_CountN_WithPositions_AppliesEach()
|
||||
{
|
||||
var positions = new[]
|
||||
{
|
||||
new[] { 0f, 0f, 0f },
|
||||
new[] { 1f, 0f, 0f },
|
||||
new[] { 2f, 0f, 0f },
|
||||
};
|
||||
|
||||
var result = GameObjectCommands.CreateGameObjects(
|
||||
name: "Batch223_Pos", primitive: "sphere", count: 3, positions: positions);
|
||||
|
||||
Assert.AreEqual(3, result.Count);
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var go = PipelineUtils.IdToObject(result.GameObjects[i].InstanceId.Value) as GameObject;
|
||||
Track(go);
|
||||
Assert.AreEqual("Batch223_Pos" + (i + 1), go.name, "count>1 must suffix Name1..NameN");
|
||||
Assert.AreEqual(new Vector3(i, 0, 0), go.transform.localPosition,
|
||||
"Each object must take its per-index position");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateGameObjects_Direct_CountN_NoPositions_StackedAtOrigin()
|
||||
{
|
||||
var result = GameObjectCommands.CreateGameObjects(name: "Batch223_Origin", count: 4);
|
||||
|
||||
Assert.AreEqual(4, result.Count);
|
||||
foreach (var item in result.GameObjects)
|
||||
{
|
||||
var go = PipelineUtils.IdToObject(item.InstanceId.Value) as GameObject;
|
||||
Track(go);
|
||||
Assert.AreEqual(Vector3.zero, go.transform.localPosition,
|
||||
"With no positions array, all objects stay at the origin");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateGameObjects_Direct_MismatchedPositionsLength_Throws()
|
||||
{
|
||||
// 2 vectors for count=3 is an agent error and must reject before creating anything.
|
||||
var positions = new[]
|
||||
{
|
||||
new[] { 0f, 0f, 0f },
|
||||
new[] { 1f, 0f, 0f },
|
||||
};
|
||||
|
||||
Assert.Throws<System.ArgumentException>(() =>
|
||||
GameObjectCommands.CreateGameObjects(name: "Batch223_Bad", count: 3, positions: positions));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateGameObjects_Direct_NullPositionEntry_ThrowsWithIndex()
|
||||
{
|
||||
// A positions array whose LENGTH matches count but contains a null entry must fail with a
|
||||
// clear, index-bearing ArgumentException (not a NullReferenceException), up front, before any
|
||||
// object is created.
|
||||
var positions = new[]
|
||||
{
|
||||
new[] { 0f, 0f, 0f },
|
||||
null,
|
||||
new[] { 2f, 0f, 0f },
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<System.ArgumentException>(() =>
|
||||
GameObjectCommands.CreateGameObjects(name: "Batch223_NullEntry", count: 3, positions: positions));
|
||||
StringAssert.Contains("positions[1]", ex.Message, "The error should name the offending index");
|
||||
|
||||
// Validation runs before creation, so nothing should have been spawned.
|
||||
var leaked = GameObjectCommands.FindGameObjects(name: "Batch223_NullEntry1");
|
||||
Assert.AreEqual(0, leaked.Count, "A rejected batch must create nothing");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateGameObjects_ViaClient_CountN_WithStringEncodedPositions()
|
||||
{
|
||||
// End-to-end: float[][] positions arrive as a JSON STRING (the CLI-219/220 coercion also
|
||||
// re-parses string-encoded nested arrays), and each object takes its per-index position.
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("create_gameobjects", new
|
||||
{
|
||||
name = "Batch223_Via",
|
||||
primitive = "cube",
|
||||
count = 3,
|
||||
positions = "[[0,0,0],[1,0,0],[2,0,0]]"
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"create_gameobjects should succeed: {response.Error}");
|
||||
|
||||
var count = response.JsonResponse["result"]?["count"]?.ToObject<int>();
|
||||
Assert.AreEqual(3, count, "Batch should report 3 created");
|
||||
|
||||
var gameObjects = response.JsonResponse["result"]?["gameObjects"] as JArray;
|
||||
Assert.IsNotNull(gameObjects, "Result should carry a gameObjects array");
|
||||
Assert.AreEqual(3, gameObjects.Count);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var instanceId = gameObjects[i]?["instanceId"]?.ToObject<ObjectId?>();
|
||||
Assert.IsTrue(instanceId.HasValue, "Each created object should carry an instanceId");
|
||||
var go = PipelineUtils.IdToObject(instanceId.Value) as GameObject;
|
||||
Track(go);
|
||||
Assert.IsNotNull(go, "Created object should be resolvable");
|
||||
Assert.AreEqual(new Vector3(i, 0, 0), go.transform.localPosition,
|
||||
"Each object must take its per-index position (string-encoded array coerced)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateGameObjects_ViaClient_CountOne_Regression()
|
||||
{
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("create_gameobjects", new { name = "Batch223_ViaOne", count = 1 });
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"create_gameobjects should succeed: {response.Error}");
|
||||
var count = response.JsonResponse["result"]?["count"]?.ToObject<int>();
|
||||
Assert.AreEqual(1, count);
|
||||
|
||||
var instanceId = response.JsonResponse["result"]?["gameObjects"]?[0]?["instanceId"]?.ToObject<ObjectId?>();
|
||||
Assert.IsTrue(instanceId.HasValue);
|
||||
var go = PipelineUtils.IdToObject(instanceId.Value) as GameObject;
|
||||
Track(go);
|
||||
Assert.AreEqual("Batch223_ViaOne", go.name, "count=1 must not append a numeric suffix");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Coercion guard (does not break plain strings / ObjectRef handles)
|
||||
|
||||
[Test]
|
||||
public void Coercion_PlainStringParam_NotReparsed()
|
||||
{
|
||||
// rename_gameobject takes a plain string 'name'. A normal string must pass straight through
|
||||
// (it does not start with '{' or '[', so the guard never touches it).
|
||||
var go = Track(new GameObject("Guard_Old"));
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("rename_gameobject", new
|
||||
{
|
||||
target = new { instanceId = PipelineUtils.GetObjectId(go) },
|
||||
name = "Guard_New"
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"rename_gameobject should succeed: {response.Error}");
|
||||
Assert.AreEqual("Guard_New", go.name, "Plain string param must apply unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Coercion_ObjectRefStringHandle_StillResolves()
|
||||
{
|
||||
// An ObjectRef passed as a bare string handle ("/Name") must continue flowing to
|
||||
// ObjectRefConverter — it does not start with '{' or '[', so the coercion leaves it alone.
|
||||
var go = Track(new GameObject("Guard_HandleTarget"));
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("set_active", new
|
||||
{
|
||||
target = "/Guard_HandleTarget",
|
||||
active = false
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess,
|
||||
$"set_active with a string handle should succeed: {response.Error}");
|
||||
Assert.IsFalse(go.activeSelf, "ObjectRef string handle must still resolve and apply");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be72bf79d228f86448e2d3fc5ed00c17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Unity.Pipeline.Editor.Commands.GameObjects;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
using Unity.Pipeline;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Editor.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the component authoring commands (CLI-192), exercised directly and via
|
||||
/// <see cref="PipelineTestServer"/>. Covers add/remove, a serialized-property round trip
|
||||
/// (set -> get) over primitive + enum + Vector types, object-reference assignment via a resolved
|
||||
/// handle, and the Undo-revert contract for a property mutation.
|
||||
/// </summary>
|
||||
public class ComponentCommandsTests
|
||||
{
|
||||
private readonly List<GameObject> m_Spawned = new List<GameObject>();
|
||||
|
||||
private GameObject Track(GameObject go)
|
||||
{
|
||||
m_Spawned.Add(go);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static ObjectRef RefTo(Object obj) => new ObjectRef { InstanceId = PipelineUtils.GetObjectId(obj) };
|
||||
|
||||
// Temp asset folder for the CLI-220 follow-up tests (asset references + arrays); cleaned in TearDown.
|
||||
private const string AssetRoot = "Assets/__CLI220Test";
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var go in m_Spawned)
|
||||
{
|
||||
if (go != null)
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
m_Spawned.Clear();
|
||||
|
||||
if (AssetDatabase.IsValidFolder(AssetRoot))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(AssetRoot);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureAssetRoot()
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder(AssetRoot))
|
||||
AssetDatabase.CreateFolder("Assets", "__CLI220Test");
|
||||
}
|
||||
|
||||
/// <summary>Pick a shader that resolves on this project's active pipeline.</summary>
|
||||
private static Shader KnownShader()
|
||||
{
|
||||
return Shader.Find("Standard")
|
||||
?? Shader.Find("Universal Render Pipeline/Lit")
|
||||
?? Shader.Find("Sprites/Default");
|
||||
}
|
||||
|
||||
/// <summary>Create a temporary Material asset under <see cref="AssetRoot"/> and return the loaded asset.</summary>
|
||||
private static Material CreateTempMaterial(string name)
|
||||
{
|
||||
EnsureAssetRoot();
|
||||
var mat = new Material(KnownShader()) { name = name };
|
||||
var path = $"{AssetRoot}/{name}.mat";
|
||||
AssetDatabase.CreateAsset(mat, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
return AssetDatabase.LoadAssetAtPath<Material>(path);
|
||||
}
|
||||
|
||||
/// <summary>Create a temporary Mesh asset (a shaderless, version-stable scalar object reference).</summary>
|
||||
private static Mesh CreateTempMesh(string name)
|
||||
{
|
||||
EnsureAssetRoot();
|
||||
var mesh = new Mesh { name = name };
|
||||
var path = $"{AssetRoot}/{name}.asset";
|
||||
AssetDatabase.CreateAsset(mesh, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
return AssetDatabase.LoadAssetAtPath<Mesh>(path);
|
||||
}
|
||||
|
||||
#region Direct
|
||||
|
||||
[Test]
|
||||
public void AddComponent_ByShortName_Adds()
|
||||
{
|
||||
var go = Track(new GameObject("AddComp_CLI192"));
|
||||
var result = ComponentCommands.AddComponent(RefTo(go), "Rigidbody");
|
||||
|
||||
Assert.IsNotNull(go.GetComponent<Rigidbody>(), "Rigidbody should be added");
|
||||
Assert.AreEqual("Rigidbody", result.Type);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddComponent_ByFullyQualifiedName_Adds()
|
||||
{
|
||||
var go = Track(new GameObject("AddCompFq_CLI192"));
|
||||
ComponentCommands.AddComponent(RefTo(go), "UnityEngine.Rigidbody");
|
||||
Assert.IsNotNull(go.GetComponent<Rigidbody>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemoveComponent_ByGameObjectAndType_Removes()
|
||||
{
|
||||
var go = Track(new GameObject("RemoveComp_CLI192"));
|
||||
go.AddComponent<Rigidbody>();
|
||||
|
||||
ComponentCommands.RemoveComponent(RefTo(go), "Rigidbody");
|
||||
|
||||
Assert.IsNull(go.GetComponent<Rigidbody>(), "Rigidbody should be removed");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetGetProperties_PrimitiveRoundTrip()
|
||||
{
|
||||
var go = Track(new GameObject("PropPrim_CLI192"));
|
||||
var rb = go.AddComponent<Rigidbody>();
|
||||
|
||||
ComponentCommands.SetComponentProperties(RefTo(rb),
|
||||
new JObject { ["m_Mass"] = 12.5f });
|
||||
|
||||
Assert.AreEqual(12.5f, rb.mass, 0.001f, "Mass should be set on the live component");
|
||||
|
||||
var read = ComponentCommands.GetComponentProperties(RefTo(rb));
|
||||
Assert.AreEqual(12.5, read.Properties["m_Mass"].ToObject<double>(), 0.001,
|
||||
"get should read back the value that set wrote");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_EnumByName_Applies()
|
||||
{
|
||||
var go = Track(new GameObject("PropEnum_CLI192"));
|
||||
var rb = go.AddComponent<Rigidbody>();
|
||||
|
||||
// m_CollisionDetection maps to the CollisionDetectionMode enum.
|
||||
ComponentCommands.SetComponentProperties(RefTo(rb),
|
||||
new JObject { ["m_CollisionDetection"] = "Continuous" });
|
||||
|
||||
Assert.AreEqual(CollisionDetectionMode.Continuous, rb.collisionDetectionMode,
|
||||
"Enum set by name should apply");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_VectorType_Applies()
|
||||
{
|
||||
var go = Track(new GameObject("PropVec_CLI192"));
|
||||
var box = go.AddComponent<BoxCollider>();
|
||||
|
||||
ComponentCommands.SetComponentProperties(RefTo(box),
|
||||
new JObject { ["m_Size"] = new JArray(2f, 3f, 4f) });
|
||||
|
||||
Assert.AreEqual(new Vector3(2, 3, 4), box.size, "Vector3 property should be set from a JSON array");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_ObjectReference_ByResolvedHandle()
|
||||
{
|
||||
// Create a real Mesh asset on disk so it resolves through ObjectResolver by path. A Mesh is
|
||||
// a version-stable, single object-reference target (MeshFilter.m_Mesh).
|
||||
const string dir = "Assets/__CLI192ObjRefTest";
|
||||
if (!AssetDatabase.IsValidFolder(dir))
|
||||
AssetDatabase.CreateFolder("Assets", "__CLI192ObjRefTest");
|
||||
var meshPath = dir + "/RefMesh.asset";
|
||||
var mesh = new Mesh { name = "RefMesh_CLI192" };
|
||||
AssetDatabase.CreateAsset(mesh, meshPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
try
|
||||
{
|
||||
var go = Track(new GameObject("PropObjRef_CLI192"));
|
||||
var mf = go.AddComponent<MeshFilter>();
|
||||
|
||||
// MeshFilter.m_Mesh is a single Mesh object reference.
|
||||
ComponentCommands.SetComponentProperties(RefTo(mf),
|
||||
new JObject { ["m_Mesh"] = new JObject { ["path"] = meshPath } });
|
||||
|
||||
Assert.IsNotNull(mf.sharedMesh, "Object reference should be assigned from the resolved handle");
|
||||
Assert.AreEqual(mesh, mf.sharedMesh, "Should be the exact resolved asset");
|
||||
}
|
||||
finally
|
||||
{
|
||||
AssetDatabase.DeleteAsset(dir);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_UnknownProperty_Throws()
|
||||
{
|
||||
var go = Track(new GameObject("PropBad_CLI192"));
|
||||
var rb = go.AddComponent<Rigidbody>();
|
||||
|
||||
Assert.Throws<System.ArgumentException>(() =>
|
||||
ComponentCommands.SetComponentProperties(RefTo(rb),
|
||||
new JObject { ["m_NoSuchProperty"] = 1 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_IsUndone()
|
||||
{
|
||||
var go = Track(new GameObject("PropUndo_CLI192"));
|
||||
var rb = go.AddComponent<Rigidbody>();
|
||||
rb.mass = 1f;
|
||||
|
||||
ComponentCommands.SetComponentProperties(RefTo(rb), new JObject { ["m_Mass"] = 9f });
|
||||
Assert.AreEqual(9f, rb.mass, 0.001f);
|
||||
|
||||
Undo.PerformUndo();
|
||||
Assert.AreEqual(1f, rb.mass, 0.001f, "Undo should revert the property change");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ViaClient
|
||||
|
||||
[Test]
|
||||
public void AddComponent_ViaClient_Succeeds()
|
||||
{
|
||||
var go = Track(new GameObject("ViaAdd_CLI192"));
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("add_component",
|
||||
new { target = new { instanceId = PipelineUtils.GetObjectId(go) }, type = "Rigidbody" });
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"add_component should succeed: {response.Error}");
|
||||
Assert.IsTrue(response.HasValidJson, "Response should have valid JSON");
|
||||
Assert.IsNotNull(go.GetComponent<Rigidbody>(), "Rigidbody should be added via client");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetComponentProperties_ViaClient_RoundTrips()
|
||||
{
|
||||
var go = Track(new GameObject("ViaProp_CLI192"));
|
||||
var rb = go.AddComponent<Rigidbody>();
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("set_component_properties", new
|
||||
{
|
||||
target = new { instanceId = PipelineUtils.GetObjectId(rb) },
|
||||
properties = new { m_Mass = 7.0 }
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"set_component_properties should succeed: {response.Error}");
|
||||
Assert.AreEqual(7f, rb.mass, 0.001f, "Mass should be set via client");
|
||||
|
||||
var massBack = response.JsonResponse["result"]?["properties"]?["m_Mass"]?.ToObject<double>();
|
||||
Assert.AreEqual(7.0, massBack.Value, 0.001, "Result should echo the committed value");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CLI-220 follow-ups — asset references + array properties
|
||||
|
||||
[Test]
|
||||
public void SetProperties_AssetReference_ByInstanceId_Assigns()
|
||||
{
|
||||
// The core of the bug: an ASSET reference addressed by instanceId must actually resolve and
|
||||
// assign (it used to silently no-op and report success).
|
||||
var mesh = CreateTempMesh("ByInstanceId");
|
||||
var go = Track(new GameObject("PropMeshInst_CLI220"));
|
||||
var mf = go.AddComponent<MeshFilter>();
|
||||
|
||||
ComponentCommands.SetComponentProperties(RefTo(mf),
|
||||
new JObject { ["m_Mesh"] = new JObject { ["instanceId"] = PipelineUtils.GetObjectId(mesh).RawValue } });
|
||||
|
||||
Assert.AreEqual(mesh, mf.sharedMesh, "Asset reference by instanceId should be assigned");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_AssetReference_ByGuid_Assigns()
|
||||
{
|
||||
var mesh = CreateTempMesh("ByGuid");
|
||||
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(mesh, out var guid, out long _);
|
||||
|
||||
var go = Track(new GameObject("PropMeshGuid_CLI220"));
|
||||
var mf = go.AddComponent<MeshFilter>();
|
||||
|
||||
ComponentCommands.SetComponentProperties(RefTo(mf),
|
||||
new JObject { ["m_Mesh"] = new JObject { ["guid"] = guid } });
|
||||
|
||||
Assert.AreEqual(mesh, mf.sharedMesh, "Asset reference by guid should be assigned");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_UnresolvableReference_Throws()
|
||||
{
|
||||
// Previously this returned success while assigning nothing. It must now fail loudly.
|
||||
var go = Track(new GameObject("PropBadRef_CLI220"));
|
||||
var mf = go.AddComponent<MeshFilter>();
|
||||
|
||||
Assert.Throws<System.ArgumentException>(() =>
|
||||
ComponentCommands.SetComponentProperties(RefTo(mf),
|
||||
new JObject { ["m_Mesh"] = new JObject { ["instanceId"] = 999999999 } }),
|
||||
"An unresolvable object reference must throw, not silently succeed.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_ArrayOfObjectReferences_Assigns()
|
||||
{
|
||||
var matA = CreateTempMaterial("ArrA");
|
||||
var matB = CreateTempMaterial("ArrB");
|
||||
|
||||
var go = Track(new GameObject("PropMatArray_CLI220"));
|
||||
var renderer = go.AddComponent<MeshRenderer>();
|
||||
|
||||
// MeshRenderer.m_Materials is an object-reference array (Generic+isArray).
|
||||
ComponentCommands.SetComponentProperties(RefTo(renderer), new JObject
|
||||
{
|
||||
["m_Materials"] = new JArray(
|
||||
new JObject { ["path"] = AssetDatabase.GetAssetPath(matA) },
|
||||
new JObject { ["path"] = AssetDatabase.GetAssetPath(matB) })
|
||||
});
|
||||
|
||||
Assert.AreEqual(2, renderer.sharedMaterials.Length, "Array property should be sized to the JSON array");
|
||||
Assert.AreEqual(matA, renderer.sharedMaterials[0]);
|
||||
Assert.AreEqual(matB, renderer.sharedMaterials[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetProperties_Array_ReadsBack()
|
||||
{
|
||||
var matA = CreateTempMaterial("ReadA");
|
||||
var matB = CreateTempMaterial("ReadB");
|
||||
|
||||
var go = Track(new GameObject("PropMatArrayRead_CLI220"));
|
||||
var renderer = go.AddComponent<MeshRenderer>();
|
||||
renderer.sharedMaterials = new[] { matA, matB };
|
||||
|
||||
var read = ComponentCommands.GetComponentProperties(RefTo(renderer));
|
||||
var materials = read.Properties["m_Materials"] as JArray;
|
||||
|
||||
Assert.IsNotNull(materials, "Array property should read back as a JSON array");
|
||||
Assert.AreEqual(2, materials.Count, "Array read should report both elements");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_ViaClient_AssetByCapitalInstanceID_Assigns()
|
||||
{
|
||||
// The agent's exact repro: a handle with a capital-D "instanceID" key. It must bind
|
||||
// (case-insensitive) and assign the asset over the wire.
|
||||
var mesh = CreateTempMesh("ViaCapital");
|
||||
var go = Track(new GameObject("PropMeshCap_CLI220"));
|
||||
var mf = go.AddComponent<MeshFilter>();
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("set_component_properties", new
|
||||
{
|
||||
target = new { instanceId = PipelineUtils.GetObjectId(mf) },
|
||||
properties = new { m_Mesh = new { instanceID = PipelineUtils.GetObjectId(mesh) } }
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"set_component_properties should succeed: {response.Error}");
|
||||
Assert.AreEqual(mesh, mf.sharedMesh, "Capital 'instanceID' handle must resolve and assign the asset");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetProperties_ViaClient_MaterialArray_Assigns()
|
||||
{
|
||||
var matA = CreateTempMaterial("ViaArrA");
|
||||
var matB = CreateTempMaterial("ViaArrB");
|
||||
var go = Track(new GameObject("PropMatArrayVia_CLI220"));
|
||||
var renderer = go.AddComponent<MeshRenderer>();
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("set_component_properties", new
|
||||
{
|
||||
target = new { instanceId = PipelineUtils.GetObjectId(renderer) },
|
||||
properties = new
|
||||
{
|
||||
m_Materials = new object[]
|
||||
{
|
||||
new { path = AssetDatabase.GetAssetPath(matA) },
|
||||
new { path = AssetDatabase.GetAssetPath(matB) }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"set_component_properties (array) should succeed: {response.Error}");
|
||||
Assert.AreEqual(2, renderer.sharedMaterials.Length, "Material array should be set via client");
|
||||
Assert.AreEqual(matA, renderer.sharedMaterials[0]);
|
||||
Assert.AreEqual(matB, renderer.sharedMaterials[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfcba8e8d69748c7a4facfd2b2ecff17
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Unity.Pipeline.Editor.Commands.GameObjects;
|
||||
using Unity.Pipeline.Models;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
using Unity.Pipeline;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Editor.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the GameObject authoring commands (CLI-192), exercised directly and via
|
||||
/// <see cref="PipelineTestServer"/>. Covers creation (empty + primitive), hierarchy build via
|
||||
/// set_parent, the find query, transform/active/tag/layer/rename mutations, deletion, and the
|
||||
/// Undo-revert contract. Test GameObjects are created with plain UnityEngine/UnityEditor APIs and
|
||||
/// torn down explicitly so the suite leaves no scene residue.
|
||||
/// </summary>
|
||||
public class GameObjectCommandsTests
|
||||
{
|
||||
private readonly List<GameObject> m_Spawned = new List<GameObject>();
|
||||
|
||||
private GameObject Track(GameObject go)
|
||||
{
|
||||
m_Spawned.Add(go);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static ObjectRef RefTo(Object obj) => new ObjectRef { InstanceId = PipelineUtils.GetObjectId(obj) };
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var go in m_Spawned)
|
||||
{
|
||||
if (go != null)
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
m_Spawned.Clear();
|
||||
}
|
||||
|
||||
#region Direct
|
||||
|
||||
[Test]
|
||||
public void CreateGameObject_Empty_ReturnsIdentity()
|
||||
{
|
||||
var result = GameObjectCommands.CreateGameObject("Empty_CLI192");
|
||||
var go = PipelineUtils.IdToObject(result.InstanceId.Value) as GameObject;
|
||||
Track(go);
|
||||
|
||||
Assert.IsNotNull(go, "Created object should be resolvable by instanceId");
|
||||
Assert.AreEqual("Empty_CLI192", go.name);
|
||||
Assert.IsNull(go.GetComponent<MeshFilter>(), "Empty GO has no mesh");
|
||||
Assert.IsNotNull(result.HierarchyPath, "Scene object should report a hierarchy path");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateGameObject_Primitive_AttachesMeshAndCollider()
|
||||
{
|
||||
var result = GameObjectCommands.CreateGameObject("Cube_CLI192", "cube");
|
||||
var go = PipelineUtils.IdToObject(result.InstanceId.Value) as GameObject;
|
||||
Track(go);
|
||||
|
||||
Assert.IsNotNull(go.GetComponent<MeshFilter>(), "Primitive cube should have a MeshFilter");
|
||||
Assert.IsNotNull(go.GetComponent<BoxCollider>(), "Primitive cube should have a BoxCollider");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateGameObject_WithParent_BuildsHierarchy()
|
||||
{
|
||||
var parent = Track(new GameObject("Parent_CLI192"));
|
||||
var childResult = GameObjectCommands.CreateGameObject("Child_CLI192", null, RefTo(parent));
|
||||
var child = PipelineUtils.IdToObject(childResult.InstanceId.Value) as GameObject;
|
||||
|
||||
Assert.AreEqual(parent.transform, child.transform.parent, "Child should be parented");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetTransform_PrimitiveChannels_Apply()
|
||||
{
|
||||
var go = Track(new GameObject("Xform_CLI192"));
|
||||
|
||||
GameObjectCommands.SetTransform(RefTo(go),
|
||||
position: new[] { 1f, 2f, 3f },
|
||||
rotation: new[] { 0f, 90f, 0f },
|
||||
scale: new[] { 2f, 2f, 2f });
|
||||
|
||||
Assert.AreEqual(new Vector3(1, 2, 3), go.transform.localPosition);
|
||||
Assert.AreEqual(new Vector3(2, 2, 2), go.transform.localScale);
|
||||
Assert.AreEqual(90f, go.transform.localEulerAngles.y, 0.01f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetParent_ToNull_DetachesToRoot()
|
||||
{
|
||||
var parent = Track(new GameObject("P_CLI192"));
|
||||
var child = Track(new GameObject("C_CLI192"));
|
||||
child.transform.SetParent(parent.transform);
|
||||
|
||||
GameObjectCommands.SetParent(RefTo(child), null);
|
||||
|
||||
Assert.IsNull(child.transform.parent, "Child should be detached to scene root");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetActive_TogglesState()
|
||||
{
|
||||
var go = Track(new GameObject("Active_CLI192"));
|
||||
GameObjectCommands.SetActive(RefTo(go), false);
|
||||
Assert.IsFalse(go.activeSelf);
|
||||
GameObjectCommands.SetActive(RefTo(go), true);
|
||||
Assert.IsTrue(go.activeSelf);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetLayer_ByIndex_Applies()
|
||||
{
|
||||
var go = Track(new GameObject("Layer_CLI192"));
|
||||
GameObjectCommands.SetLayer(RefTo(go), "5"); // UI layer index
|
||||
Assert.AreEqual(5, go.layer);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetTag_UnknownTag_Throws()
|
||||
{
|
||||
var go = Track(new GameObject("Tag_CLI192"));
|
||||
Assert.Throws<System.ArgumentException>(
|
||||
() => GameObjectCommands.SetTag(RefTo(go), "__definitely_not_a_real_tag__"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Rename_ChangesName()
|
||||
{
|
||||
var go = Track(new GameObject("Old_CLI192"));
|
||||
GameObjectCommands.RenameGameObject(RefTo(go), "New_CLI192");
|
||||
Assert.AreEqual("New_CLI192", go.name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindGameObjects_ByNameAndType_Filters()
|
||||
{
|
||||
var go = Track(new GameObject("Findable_CLI192"));
|
||||
go.AddComponent<Rigidbody>();
|
||||
|
||||
var result = GameObjectCommands.FindGameObjects(name: "Findable_CLI192", type: "Rigidbody");
|
||||
|
||||
Assert.AreEqual(1, result.Count);
|
||||
Assert.AreEqual(PipelineUtils.GetObjectId(go), result.GameObjects[0].InstanceId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Delete_RemovesFromScene()
|
||||
{
|
||||
var go = new GameObject("Doomed_CLI192");
|
||||
var id = PipelineUtils.GetObjectId(go);
|
||||
GameObjectCommands.DeleteGameObject(new ObjectRef { InstanceId = id });
|
||||
|
||||
Assert.IsNull(PipelineUtils.IdToObject(id), "GameObject should be destroyed");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetTransform_IsUndone()
|
||||
{
|
||||
var go = Track(new GameObject("Undo_CLI192"));
|
||||
go.transform.localPosition = Vector3.zero;
|
||||
|
||||
GameObjectCommands.SetTransform(RefTo(go), position: new[] { 5f, 5f, 5f });
|
||||
Assert.AreEqual(new Vector3(5, 5, 5), go.transform.localPosition);
|
||||
|
||||
Undo.PerformUndo();
|
||||
Assert.AreEqual(Vector3.zero, go.transform.localPosition, "Undo should revert the transform change");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ViaClient
|
||||
|
||||
[Test]
|
||||
public void CreateGameObject_ViaClient_Succeeds()
|
||||
{
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("create_gameobject", new { name = "ViaClient_CLI192", primitive = "sphere" });
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"create_gameobject should succeed: {response.Error}");
|
||||
Assert.IsTrue(response.HasValidJson, "Response should have valid JSON");
|
||||
Assert.IsTrue(response.JsonResponse.ContainsKey("result"), "Should have result field");
|
||||
|
||||
var instanceId = response.JsonResponse["result"]?["instanceId"]?.ToObject<ObjectId?>();
|
||||
Assert.IsTrue(instanceId.HasValue, "Result should carry an instanceId");
|
||||
var go = PipelineUtils.IdToObject(instanceId.Value) as GameObject;
|
||||
Track(go);
|
||||
Assert.IsNotNull(go, "Created object should be resolvable");
|
||||
Assert.IsNotNull(go.GetComponent<SphereCollider>(), "Sphere primitive should have a SphereCollider");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindGameObjects_ViaClient_ReturnsMatches()
|
||||
{
|
||||
var go = Track(new GameObject("ViaFind_CLI192"));
|
||||
|
||||
using (var server = new PipelineTestServer())
|
||||
{
|
||||
var response = server.Execute("find_gameobjects", new { name = "ViaFind_CLI192" });
|
||||
|
||||
Assert.IsTrue(response.IsSuccess, $"find_gameobjects should succeed: {response.Error}");
|
||||
var count = response.JsonResponse["result"]?["count"]?.ToObject<int>();
|
||||
Assert.AreEqual(1, count, "Should find exactly the one named GameObject");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 661e4b429e2643e185927cf6459c3e25
|
||||
Reference in New Issue
Block a user