Files
aibis-dream/Assets/Tests/FrameAnimation/EditMode/FrameAnimationCoreTests.cs
T

259 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Tests.EditMode
{
public sealed class FrameAnimationCoreTests
{
private readonly List<UnityEngine.Object> createdObjects = new List<UnityEngine.Object>();
[TearDown]
public void TearDown()
{
foreach (var createdObject in createdObjects.Where(item => item != null))
{
UnityEngine.Object.DestroyImmediate(createdObject);
}
createdObjects.Clear();
}
[Test]
public void DataModel_SerializesOptionalOverridesAndKeepsInternalIds()
{
var clip = CreateClip("Idle", FrameClipEndBehavior.Loop, 1f, 100, 100);
var node = new AnimationNode(
clip.Id,
endBehaviorOverride: FrameClipEndBehavior.HideTarget,
speedOverride: 2f);
var flow = new AnimationFlow("Flow", "Flow", node.InternalId, FrameClipEndBehavior.Clear);
var graph = CreateGraph("Graph", new[] { clip }, new[] { node }, Array.Empty<AnimationEdge>(), new[] { flow }, flow.Id);
var clone = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
var json = EditorJsonUtility.ToJson(graph);
EditorJsonUtility.FromJsonOverwrite(json, clone);
Assert.That(clone.Nodes[0].InternalId, Is.EqualTo(node.InternalId));
Assert.That(clone.Nodes[0].EndBehaviorOverride, Is.EqualTo(FrameClipEndBehavior.HideTarget));
Assert.That(clone.Nodes[0].SpeedOverride, Is.EqualTo(2f));
Assert.That(clone.Flows[0].EndBehaviorOverride, Is.EqualTo(FrameClipEndBehavior.Clear));
}
[Test]
public void Validator_ReportsDuplicatePlayableAndInvalidGraphStructure()
{
var first = CreateClip("Same", FrameClipEndBehavior.HoldLastFrame, 1f, 100);
var second = CreateClip("Same", FrameClipEndBehavior.HoldLastFrame, 1f);
var node = new AnimationNode("MissingClip", internalId: "invalid-id");
var flow = new AnimationFlow("Same", "Same Flow", node.InternalId);
var graph = CreateGraph(
"Invalid",
new[] { first, second },
new[] { node },
Array.Empty<AnimationEdge>(),
new[] { flow },
"Missing");
var report = FrameAnimationGraphValidator.Validate(graph);
Assert.That(report.HasErrors, Is.True);
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.PlayableIdDuplicate), Is.True);
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.ClipFramesEmpty), Is.True);
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.InternalIdInvalid), Is.True);
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.NodeClipMissing), Is.True);
Assert.That(report.Issues.Any(issue => issue.Code == FrameAnimationValidationCode.DefaultPlayableInvalid), Is.True);
}
[Test]
public void Resolver_RejectsCycleAndMultipleSuccessors()
{
var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100);
var a = new AnimationNode(clip.Id);
var b = new AnimationNode(clip.Id);
var c = new AnimationNode(clip.Id);
var flow = new AnimationFlow("Flow", "Flow", a.InternalId);
var graph = CreateGraph(
"Graph",
new[] { clip },
new[] { a, b, c },
new[]
{
new AnimationEdge(a.InternalId, b.InternalId),
new AnimationEdge(a.InternalId, c.InternalId),
new AnimationEdge(b.InternalId, a.InternalId)
},
new[] { flow },
flow.Id);
var resolved = FrameAnimationResolver.TryResolve(
graph,
flow.Id,
default,
out _,
out var error);
Assert.That(resolved, Is.False);
Assert.That(error.Code, Is.EqualTo(FrameAnimationPlaybackErrorCode.InvalidPlayableData));
}
[Test]
public void Session_LargeDeltaCrossesFlowAndLoopsTerminalClip()
{
var intro = CreateClip("Intro", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 100);
var idle = CreateClip("Idle", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 100, 100);
var introNode = new AnimationNode(intro.Id);
var idleNode = new AnimationNode(idle.Id, endBehaviorOverride: FrameClipEndBehavior.Loop);
var flow = new AnimationFlow("IntroToIdle", "Intro To Idle", introNode.InternalId);
var graph = CreateGraph(
"Graph",
new[] { intro, idle },
new[] { introNode, idleNode },
new[] { new AnimationEdge(introNode.InternalId, idleNode.InternalId) },
new[] { flow },
flow.Id);
Assert.That(
FrameAnimationResolver.TryResolve(graph, flow.Id, default, out var plan, out var error),
Is.True,
error.ToString());
var session = new FrameAnimationPlaybackSession(plan);
session.Start(_ => { });
var evaluation = session.Evaluate(1.05d, 1f, _ => { });
Assert.That(evaluation.IsCompleted, Is.False);
Assert.That(session.CurrentClipId, Is.EqualTo(idle.Id));
Assert.That(session.CurrentNodeId, Is.EqualTo(idleNode.InternalId));
Assert.That(session.CurrentFrameIndex, Is.EqualTo(2));
}
[Test]
public void Session_ExactBoundaryAndRemainingTimeAdvanceDeterministically()
{
var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100, 200, 300);
var graph = CreateGraph(
"Graph",
new[] { clip },
Array.Empty<AnimationNode>(),
Array.Empty<AnimationEdge>(),
Array.Empty<AnimationFlow>(),
clip.Id);
Assert.That(FrameAnimationResolver.TryResolve(graph, clip.Id, default, out var plan, out _), Is.True);
var session = new FrameAnimationPlaybackSession(plan);
session.Start(_ => { });
session.Evaluate(0.1d, 1f, _ => { });
Assert.That(session.CurrentFrameIndex, Is.EqualTo(1));
session.Evaluate(0.25d, 1f, _ => { });
Assert.That(session.CurrentFrameIndex, Is.EqualTo(2));
var completion = session.Evaluate(0.25d, 1f, _ => { });
Assert.That(completion.IsCompleted, Is.True);
Assert.That(completion.EndBehavior, Is.EqualTo(FrameClipEndBehavior.HoldLastFrame));
}
[Test]
public void Session_UsesNodeSpeedInsteadOfClipSpeedAndKeepsPlayerMultiplier()
{
var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 0.5f, 100, 100);
var node = new AnimationNode(clip.Id, speedOverride: 2f);
var flow = new AnimationFlow("Flow", "Flow", node.InternalId);
var graph = CreateGraph(
"Graph",
new[] { clip },
new[] { node },
Array.Empty<AnimationEdge>(),
new[] { flow },
flow.Id);
Assert.That(FrameAnimationResolver.TryResolve(graph, flow.Id, default, out var plan, out _), Is.True);
var session = new FrameAnimationPlaybackSession(plan);
session.Start(_ => { });
session.Evaluate(0.025d, 2f, _ => { });
Assert.That(session.CurrentFrameIndex, Is.EqualTo(1));
}
[Test]
public void Resolver_EndBehaviorPrecedenceIsNodeThenRequestThenFlowThenClip()
{
var clip = CreateClip("Clip", FrameClipEndBehavior.HoldLastFrame, 1f, 100);
var node = new AnimationNode(clip.Id, endBehaviorOverride: FrameClipEndBehavior.HideTarget);
var flow = new AnimationFlow("Flow", "Flow", node.InternalId, FrameClipEndBehavior.Clear);
var graph = CreateGraph("Graph", new[] { clip }, new[] { node }, Array.Empty<AnimationEdge>(), new[] { flow }, flow.Id);
Assert.That(
FrameAnimationResolver.TryResolve(
graph,
flow.Id,
new FrameAnimationPlayOptions(FrameClipEndBehavior.Loop),
out var plan,
out _),
Is.True);
Assert.That(plan.Steps[0].TerminalEndBehavior, Is.EqualTo(FrameClipEndBehavior.HideTarget));
}
[Test]
public void Handle_CompletesOnceAndLateObserversReceiveSameResult()
{
var handle = new FrameAnimationPlaybackHandle(42);
var callbackCount = 0;
FrameAnimationPlaybackResult observed = default;
handle.RegisterCompleted(result =>
{
callbackCount++;
observed = result;
});
Assert.Throws<InvalidOperationException>(() => _ = handle.Result);
var completion = new FrameAnimationPlaybackResult(
42,
"Clip",
FrameAnimationCompletionReason.Completed,
FrameAnimationPlaybackError.None);
Assert.That(handle.TryComplete(completion), Is.True);
Assert.That(handle.TryComplete(completion), Is.False);
handle.RegisterCompleted(_ => callbackCount++);
Assert.That(callbackCount, Is.EqualTo(2));
Assert.That(observed.RequestId, Is.EqualTo(42));
Assert.That(handle.WaitAsync().Result.RequestId, Is.EqualTo(42));
}
private FrameClip CreateClip(
string id,
FrameClipEndBehavior endBehavior,
float speed,
params int[] durations)
{
var clip = Track(ScriptableObject.CreateInstance<FrameClip>());
var frames = durations.Select((duration, index) =>
new FrameAnimationFrame(null, duration, $"frame_{index}", index));
clip.Configure(id, id, frames, speed, endBehavior);
return clip;
}
private FrameAnimationGraph CreateGraph(
string id,
IEnumerable<FrameClip> clips,
IEnumerable<AnimationNode> nodes,
IEnumerable<AnimationEdge> edges,
IEnumerable<AnimationFlow> flows,
string defaultPlayableId)
{
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
graph.Configure(id, id, clips, nodes, edges, flows, defaultPlayableId);
return graph;
}
private T Track<T>(T value) where T : UnityEngine.Object
{
createdObjects.Add(value);
return value;
}
}
}