Initial commit: AIGame Unity client with URP, HybridCLR, YooAsset and tooling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+336
@@ -0,0 +1,336 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using Unity.Pipeline.Commands;
|
||||
using Unity.Pipeline.Editor;
|
||||
using UnityEditor;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for registering commands dynamically, without a [CliCommand] attribute.
|
||||
/// A dynamic registration must be indistinguishable from an attribute-declared one:
|
||||
/// same metadata, same [CliArg] parameter discovery, same execution path.
|
||||
/// </summary>
|
||||
public class DynamicCommandRegistrationTests
|
||||
{
|
||||
private readonly List<string> m_Registered = new List<string>();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
CommandRegistry.SetDiscovery(new TypeCacheCommandDiscovery());
|
||||
}
|
||||
|
||||
[UnityTearDown]
|
||||
public IEnumerator TearDown()
|
||||
{
|
||||
// A play-mode test that fails mid-session would otherwise leave the Editor playing and
|
||||
// poison every later test.
|
||||
if (EditorApplication.isPlaying)
|
||||
yield return new ExitPlayMode();
|
||||
|
||||
// Dynamic registrations outlive a test; leaking one would corrupt every later
|
||||
// test that enumerates the registry.
|
||||
foreach (var name in m_Registered)
|
||||
CommandRegistry.UnregisterCommand(name);
|
||||
m_Registered.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Register a command and remember its name so TearDown can clean it up.</summary>
|
||||
private void Register(string name, string description, Delegate handler,
|
||||
bool mainThreadRequired = true, bool runtimeOnly = false, string[] tags = null)
|
||||
{
|
||||
CommandRegistry.RegisterCommand(name, description, handler, mainThreadRequired, runtimeOnly, tags);
|
||||
m_Registered.Add(name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_StaticHandler_IsDiscoveredWithItsMetadata()
|
||||
{
|
||||
Register("test_dynamic_add", "Add two numbers",
|
||||
new Func<int, int, int>(DynamicHandlers.Add),
|
||||
mainThreadRequired: false, runtimeOnly: true, tags: new[] { "test/dynamic" });
|
||||
|
||||
var command = CommandRegistry.DiscoverCommands().FirstOrDefault(c => c.Name == "test_dynamic_add");
|
||||
|
||||
Assert.IsNotNull(command, "Dynamically registered command should be discoverable");
|
||||
Assert.AreEqual("Add two numbers", command.Description);
|
||||
Assert.IsFalse(command.MainThreadRequired, "MainThreadRequired should come from the register call");
|
||||
Assert.IsTrue(command.RuntimeOnly, "RuntimeOnly should come from the register call");
|
||||
CollectionAssert.AreEqual(new[] { "test/dynamic" }, command.Tags);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_OmittedTags_YieldsEmptyTagList()
|
||||
{
|
||||
Register("test_dynamic_untagged", "No tags", new Action(DynamicHandlers.Noop));
|
||||
|
||||
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "test_dynamic_untagged");
|
||||
|
||||
Assert.IsNotNull(command.Tags, "Tags should never be null");
|
||||
Assert.AreEqual(0, command.Tags.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_HandlerParameters_ComeFromCliArgAttributes()
|
||||
{
|
||||
Register("test_dynamic_add", "Add two numbers",
|
||||
new Func<int, int, int>(DynamicHandlers.Add), tags: new[] { "test/dynamic" });
|
||||
|
||||
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "test_dynamic_add");
|
||||
|
||||
Assert.AreEqual(2, command.Parameters.Count);
|
||||
|
||||
var left = command.Parameters[0];
|
||||
Assert.AreEqual("left", left.Name);
|
||||
Assert.AreEqual("Left operand", left.Description);
|
||||
Assert.IsTrue(left.Required, "Required should be read from the [CliArg] attribute");
|
||||
Assert.AreEqual(typeof(int), left.ParameterType);
|
||||
|
||||
var right = command.Parameters[1];
|
||||
Assert.AreEqual("right", right.Name);
|
||||
Assert.AreEqual("Right operand", right.Description);
|
||||
Assert.IsFalse(right.Required, "A parameter with a default value is optional");
|
||||
Assert.AreEqual(10, right.DefaultValue);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_DerivesPackageFromHandlerAssembly()
|
||||
{
|
||||
Register("test_dynamic_untagged", "No tags", new Action(DynamicHandlers.Noop));
|
||||
|
||||
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "test_dynamic_untagged");
|
||||
|
||||
Assert.AreEqual("Unity.Pipeline.Tests.Editor", command.Package);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_InstanceHandler_CarriesTheBoundInstanceAsTarget()
|
||||
{
|
||||
var echo = new Echoer("prefix");
|
||||
|
||||
Register("test_dynamic_echo", "Echo a message",
|
||||
new Func<string, string>(echo.Echo), tags: new[] { "test/dynamic" });
|
||||
|
||||
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "test_dynamic_echo");
|
||||
|
||||
Assert.AreSame(echo, command.Target,
|
||||
"An instance-method delegate must carry its receiver so the invoke site can bind it");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_StaticHandler_HasNullTarget()
|
||||
{
|
||||
Register("test_dynamic_untagged", "No tags", new Action(DynamicHandlers.Noop));
|
||||
|
||||
var command = CommandRegistry.DiscoverCommands().First(c => c.Name == "test_dynamic_untagged");
|
||||
|
||||
Assert.IsNull(command.Target, "A static handler has no receiver");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RegisterCommand_InstanceHandler_ExecutesThroughApiExec()
|
||||
{
|
||||
var echo = new Echoer("prefix");
|
||||
Register("test_dynamic_echo", "Echo a message",
|
||||
new Func<string, string>(echo.Echo),
|
||||
mainThreadRequired: false, tags: new[] { "test/dynamic" });
|
||||
|
||||
var server = new TestEditorPipelineServer();
|
||||
server.Start();
|
||||
var client = new Unity.Pipeline.Tests.Runtime.PipelineClient(server);
|
||||
try
|
||||
{
|
||||
var response = await client.ExecuteCommandAsync("test_dynamic_echo", new { message = "hello" });
|
||||
|
||||
Assert.IsTrue(response.IsSuccess,
|
||||
$"Exec should succeed, got: {response.Error} {response.RawResponse}");
|
||||
var json = JObject.Parse(response.RawResponse);
|
||||
Assert.AreEqual("prefix:hello", json["result"]?.ToString(),
|
||||
"The command must be invoked on the bound instance, not on a null receiver");
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.Dispose();
|
||||
server.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_NameAlreadyRegisteredDynamically_Throws()
|
||||
{
|
||||
Register("test_dynamic_untagged", "No tags", new Action(DynamicHandlers.Noop));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
CommandRegistry.RegisterCommand("test_dynamic_untagged", "Duplicate",
|
||||
new Action(DynamicHandlers.Noop)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_NameOfAttributeCommand_Throws()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
CommandRegistry.RegisterCommand("log_editor", "Shadowing a shipped command",
|
||||
new Action(DynamicHandlers.Noop)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_NullArguments_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
CommandRegistry.RegisterCommand(null, "Description", new Action(DynamicHandlers.Noop)));
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
CommandRegistry.RegisterCommand("test_dynamic_null", null, new Action(DynamicHandlers.Noop)));
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
CommandRegistry.RegisterCommand("test_dynamic_null", "Description", null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_EmptyName_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
CommandRegistry.RegisterCommand(" ", "Description", new Action(DynamicHandlers.Noop)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnregisterCommand_RemovesTheCommandFromDiscovery()
|
||||
{
|
||||
Register("test_dynamic_untagged", "No tags", new Action(DynamicHandlers.Noop));
|
||||
Assert.IsTrue(CommandRegistry.DiscoverCommands().Any(c => c.Name == "test_dynamic_untagged"),
|
||||
"Precondition: the command is registered");
|
||||
|
||||
var removed = CommandRegistry.UnregisterCommand("test_dynamic_untagged");
|
||||
|
||||
Assert.IsTrue(removed, "Unregistering a dynamic command should report success");
|
||||
Assert.IsFalse(CommandRegistry.DiscoverCommands().Any(c => c.Name == "test_dynamic_untagged"),
|
||||
"Command should be gone from discovery");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnregisterCommand_UnknownName_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(CommandRegistry.UnregisterCommand("test_dynamic_never_registered"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnregisterCommand_AttributeCommand_ReturnsFalseAndKeepsIt()
|
||||
{
|
||||
var removed = CommandRegistry.UnregisterCommand("log_editor");
|
||||
|
||||
Assert.IsFalse(removed, "Attribute-declared commands are not removable");
|
||||
Assert.IsTrue(CommandRegistry.DiscoverCommands().Any(c => c.Name == "log_editor"),
|
||||
"The attribute command must survive the unregister attempt");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_SurvivesClearCache()
|
||||
{
|
||||
Register("test_dynamic_untagged", "No tags", new Action(DynamicHandlers.Noop));
|
||||
|
||||
CommandRegistry.ClearCache();
|
||||
|
||||
Assert.IsTrue(CommandRegistry.DiscoverCommands().Any(c => c.Name == "test_dynamic_untagged"),
|
||||
"Dynamic registrations must survive a cache clear (e.g. a discovery-mechanism swap)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_SurvivesSetDiscovery()
|
||||
{
|
||||
Register("test_dynamic_untagged", "No tags", new Action(DynamicHandlers.Noop));
|
||||
|
||||
CommandRegistry.SetDiscovery(new TypeCacheCommandDiscovery());
|
||||
|
||||
Assert.IsTrue(CommandRegistry.DiscoverCommands().Any(c => c.Name == "test_dynamic_untagged"),
|
||||
"Dynamic registrations must survive SetDiscovery");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterCommand_DoesNotDisturbAttributeCommands()
|
||||
{
|
||||
var before = CommandRegistry.DiscoverCommands().Count(c => c.Name == "log_editor");
|
||||
|
||||
Register("test_dynamic_untagged", "No tags", new Action(DynamicHandlers.Noop));
|
||||
|
||||
Assert.AreEqual(before, CommandRegistry.DiscoverCommands().Count(c => c.Name == "log_editor"),
|
||||
"Registering a dynamic command must not duplicate or drop attribute commands");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator RegisterCommand_InPlayMode_IsClearedOnPlayModeExit()
|
||||
{
|
||||
yield return new EnterPlayMode();
|
||||
|
||||
Register("test_dynamic_play_session", "Registered from a Play session",
|
||||
new Action(DynamicHandlers.Noop));
|
||||
|
||||
yield return new ExitPlayMode();
|
||||
|
||||
Assert.IsFalse(CommandRegistry.DiscoverCommands().Any(c => c.Name == "test_dynamic_play_session"),
|
||||
"A registration made in Play Mode must not outlive the session: whatever registered " +
|
||||
"it (Awake/OnEnable) runs again on the next entry, and the surviving delegate would " +
|
||||
"be bound to that session's destroyed object.");
|
||||
|
||||
// The reported symptom: with domain reload disabled, the second entry threw because the
|
||||
// first entry's registration was still there.
|
||||
yield return new EnterPlayMode();
|
||||
|
||||
Assert.DoesNotThrow(
|
||||
() => Register("test_dynamic_play_session", "Registered from the next Play session",
|
||||
new Action(DynamicHandlers.Noop)),
|
||||
"Re-registering the same name on a later Play Mode entry must not throw");
|
||||
|
||||
yield return new ExitPlayMode();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator RegisterCommand_InEditMode_SurvivesAPlaySession()
|
||||
{
|
||||
Register("test_dynamic_edit_scoped", "Registered from Edit Mode",
|
||||
new Action(DynamicHandlers.Noop));
|
||||
|
||||
yield return new EnterPlayMode();
|
||||
yield return new ExitPlayMode();
|
||||
|
||||
Assert.IsTrue(CommandRegistry.DiscoverCommands().Any(c => c.Name == "test_dynamic_edit_scoped"),
|
||||
"A registration made in Edit Mode must survive a Play session: an " +
|
||||
"[InitializeOnLoadMethod] runs once per domain, so nothing would restore it.");
|
||||
}
|
||||
|
||||
/// <summary>Static handlers used as dynamic command implementations.</summary>
|
||||
private static class DynamicHandlers
|
||||
{
|
||||
public static int Add(
|
||||
[CliArg("left", "Left operand", Required = true)] int left,
|
||||
[CliArg("right", "Right operand")] int right = 10)
|
||||
{
|
||||
return left + right;
|
||||
}
|
||||
|
||||
public static void Noop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Instance handler, to verify the receiver survives registration and invocation.</summary>
|
||||
private sealed class Echoer
|
||||
{
|
||||
private readonly string m_Prefix;
|
||||
|
||||
public Echoer(string prefix)
|
||||
{
|
||||
m_Prefix = prefix;
|
||||
}
|
||||
|
||||
public string Echo([CliArg("message", "The message to echo", Required = true)] string message)
|
||||
{
|
||||
return $"{m_Prefix}:{message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user