using NUnit.Framework;
using Newtonsoft.Json.Linq;
using Unity.Pipeline.Editor.Authoring;
using Unity.Pipeline.Editor.Commands.Animation;
using Unity.Pipeline.Models;
using UnityEditor;
using UnityEngine;
namespace Unity.Pipeline.Tests.Editor.Animation
{
///
/// Tests for CLI-214 Group B (animator controllers): create controller (Base Layer, no states),
/// add parameter (and duplicate => duplicate_parameter), add layer, add state (with motion + as
/// default), add transition (with a matching condition), and the validation failures (missing
/// parameter, mode/type mismatch). Assets are generated in-test and cleaned up.
///
public class AnimatorControllerCommandsTests
{
private const string Root = "Assets/__CLI214CtrlTest";
[TearDown]
public void TearDown()
{
ProjectPaths.ResetAuthoringRoot();
if (AssetDatabase.IsValidFolder(Root))
{
AssetDatabase.DeleteAsset(Root);
AssetDatabase.Refresh();
}
}
private static ObjectRef PathRef(string path) => new ObjectRef { Path = path };
private string CreateController(string name)
{
var path = $"{Root}/{name}.controller";
AnimatorControllerCommands.CreateAnimatorController(path);
return path;
}
[Test]
public void CreateController_HasOneBaseLayerNoStates()
{
var path = CreateController("Empty");
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
Assert.AreEqual(1, info.Layers.Count, "A new controller should have exactly one (Base) layer");
Assert.AreEqual(0, info.Layers[0].States.Count, "Base Layer should start with no states");
Assert.AreEqual(0, info.Parameters.Count);
}
[Test]
public void AddParameter_Float_AndDuplicate()
{
var path = CreateController("Params");
var added = AnimatorControllerCommands.AddAnimatorParameter(PathRef(path), "Speed", "Float", new JValue(0f));
Assert.IsInstanceOf(added);
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
Assert.AreEqual(1, info.Parameters.Count);
Assert.AreEqual("Speed", info.Parameters[0].Name);
Assert.AreEqual("Float", info.Parameters[0].ParamType);
// Duplicate name => structured error, not exception.
var dup = AnimatorControllerCommands.AddAnimatorParameter(PathRef(path), "Speed", "Float");
Assert.IsInstanceOf(dup);
Assert.AreEqual("duplicate_parameter", ((ErrorResult)dup).Code);
}
[Test]
public void AddLayer_AppendsLayer()
{
var path = CreateController("Layers");
var added = AnimatorControllerCommands.AddAnimatorLayer(PathRef(path), "Upper", weight: 0.5f, blendingMode: "Additive");
Assert.IsInstanceOf(added);
Assert.AreEqual(1, ((AddLayerResult)added).Layer.Index);
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
Assert.AreEqual(2, info.Layers.Count);
Assert.AreEqual("Upper", info.Layers[1].Name);
}
[Test]
public void AddState_WithMotionAndDefault()
{
var path = CreateController("States");
// A clip to use as the state motion.
var clipPath = Root + "/Idle.anim";
AnimationClipCommands.CreateAnimationClip(clipPath);
var added = AnimatorControllerCommands.AddAnimatorState(
PathRef(path), layer: null, name: "Idle", motion: PathRef(clipPath), isDefault: true);
Assert.IsInstanceOf(added);
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
Assert.AreEqual(1, info.Layers[0].States.Count);
var state = info.Layers[0].States[0];
Assert.AreEqual("Idle", state.Name);
Assert.IsTrue(state.IsDefault, "Idle should be the layer default");
Assert.IsNotNull(state.Motion, "Idle should have a non-null motion");
Assert.AreEqual("Idle", info.Layers[0].DefaultState);
}
[Test]
public void AddState_UnknownLayerName_ReturnsLayerNotFound()
{
var path = CreateController("BadLayer");
var result = AnimatorControllerCommands.AddAnimatorState(
PathRef(path), layer: new JValue("NoSuchLayer"), name: "S");
Assert.IsInstanceOf(result);
Assert.AreEqual("layer_not_found", ((ErrorResult)result).Code);
}
[Test]
public void AddTransition_WithCondition()
{
var path = CreateController("Transitions");
AnimatorControllerCommands.AddAnimatorParameter(PathRef(path), "Speed", "Float", new JValue(0f));
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Idle");
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Run");
var conditions = new JArray
{
new JObject { ["parameter"] = "Speed", ["mode"] = "Greater", ["threshold"] = 0.1f }
};
var added = AnimatorControllerCommands.AddAnimatorTransition(
PathRef(path), null, "Idle", "Run", conditions);
Assert.IsInstanceOf(added);
Assert.AreEqual(1, ((AddTransitionResult)added).Transition.ConditionCount);
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
var transitions = info.Layers[0].Transitions;
Assert.AreEqual(1, transitions.Count);
Assert.AreEqual("Idle", transitions[0].From);
Assert.AreEqual("Run", transitions[0].To);
Assert.AreEqual(1, transitions[0].Conditions.Count);
Assert.AreEqual("Speed", transitions[0].Conditions[0].Parameter);
Assert.AreEqual("Greater", transitions[0].Conditions[0].Mode);
}
[Test]
public void AddTransition_UnknownParameter_ThrowsAndWritesNothing()
{
var path = CreateController("BadParam");
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Idle");
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Run");
var conditions = new JArray
{
new JObject { ["parameter"] = "Ghost", ["mode"] = "Greater", ["threshold"] = 0.1f }
};
var ex = Assert.Throws(
() => AnimatorControllerCommands.AddAnimatorTransition(PathRef(path), null, "Idle", "Run", conditions));
StringAssert.Contains("Ghost", ex.Message, "error should name the offending parameter");
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
Assert.AreEqual(0, info.Layers[0].Transitions.Count, "nothing should be written on a bad condition");
}
[Test]
public void AddTransition_ModeTypeMismatch_Throws()
{
var path = CreateController("Mismatch");
AnimatorControllerCommands.AddAnimatorParameter(PathRef(path), "Speed", "Float", new JValue(0f));
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Idle");
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Run");
// "If" is only valid for Bool/Trigger, not a Float parameter.
var conditions = new JArray
{
new JObject { ["parameter"] = "Speed", ["mode"] = "If" }
};
Assert.Throws(
() => AnimatorControllerCommands.AddAnimatorTransition(PathRef(path), null, "Idle", "Run", conditions));
}
[Test]
public void AddState_FractionalLayerIndex_ReturnsLayerNotFound()
{
var path = CreateController("FractionalLayer");
// 0.9 must NOT be silently rounded to layer 1; a non-integer index is rejected.
var result = AnimatorControllerCommands.AddAnimatorState(
PathRef(path), layer: new JValue(0.9d), name: "S");
Assert.IsInstanceOf(result);
Assert.AreEqual("layer_not_found", ((ErrorResult)result).Code);
}
[Test]
public void AddState_IntegerValuedFloatLayer_Resolves()
{
var path = CreateController("IntFloatLayer");
// 0.0 is an integer-valued float and resolves to the Base Layer (index 0).
var added = AnimatorControllerCommands.AddAnimatorState(
PathRef(path), layer: new JValue(0d), name: "Idle");
Assert.IsInstanceOf(added);
Assert.AreEqual(0, ((AddStateResult)added).State.Layer);
}
[Test]
public void AddTransition_ExitTimeOutOfRange_Throws()
{
var path = CreateController("BadExitTime");
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Idle");
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Run");
Assert.Throws(
() => AnimatorControllerCommands.AddAnimatorTransition(
PathRef(path), null, "Idle", "Run", conditions: null, hasExitTime: true, exitTime: 1.5f));
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
Assert.AreEqual(0, info.Layers[0].Transitions.Count, "an out-of-range exitTime must write nothing");
}
[Test]
public void AddTransition_NegativeDuration_Throws()
{
var path = CreateController("BadDuration");
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Idle");
AnimatorControllerCommands.AddAnimatorState(PathRef(path), null, "Run");
Assert.Throws(
() => AnimatorControllerCommands.AddAnimatorTransition(
PathRef(path), null, "Idle", "Run", conditions: null, duration: -1f));
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
Assert.AreEqual(0, info.Layers[0].Transitions.Count, "a negative duration must write nothing");
}
[Test]
public void AddParameter_DryRun_WritesNothing()
{
var path = CreateController("DryParam");
AnimatorControllerCommands.AddAnimatorParameter(PathRef(path), "Speed", "Float", new JValue(0f), dryRun: true);
var info = AnimatorControllerCommands.GetAnimatorController(PathRef(path));
Assert.AreEqual(0, info.Parameters.Count, "dry_run must not write a parameter");
}
[Test]
public void CreateController_ViaClient_Succeeds()
{
using (var server = new PipelineTestServer())
{
var path = Root + "/ViaClient/Made.controller";
var response = server.Execute("create_animator_controller", new { path });
Assert.IsTrue(response.IsSuccess, $"create_animator_controller should succeed: {response.Error}");
Assert.IsNotNull(AssetDatabase.LoadMainAssetAtPath(path), "Controller should exist on disk");
}
}
}
}