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,303 @@
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Editor;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace Unity.Pipeline.Tests.Editor
{
/// <summary>
/// Tests for command registration system using [CliCommand] and [CliArg] attributes.
/// These test the attribute-based command definition that CLI tools will discover.
/// </summary>
public class CommandRegistrationTests
{
[SetUp]
public void SetUp()
{
// Ensure TypeCache discovery is available for tests
CommandRegistry.SetDiscovery(new TypeCacheCommandDiscovery());
}
[Test]
public void CliCommandAttribute_AppliedToMethod_CanBeRetrievedViaReflection()
{
// Arrange - Get the test command method (private static, resolved via reflection)
var methodInfo = typeof(DummyCommands).GetMethod("TestLogCommand",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(methodInfo, "Test command method should exist");
// Act - Try to get the CliCommand attribute
var attribute = methodInfo.GetCustomAttribute<CliCommandAttribute>();
// Assert - Attribute should be present and contain expected data
Assert.IsNotNull(attribute, "Method should have CliCommand attribute");
Assert.AreEqual("log_editor", attribute.Name, "Command name should match attribute");
Assert.AreEqual("Log a message to Unity Editor console", attribute.Description,
"Command description should match attribute");
}
[Test]
public void CliArgAttribute_AppliedToParameter_CanBeRetrievedViaReflection()
{
// Arrange - Get the test command method and its parameters (private static)
var methodInfo = typeof(DummyCommands).GetMethod("TestLogCommand",
BindingFlags.NonPublic | BindingFlags.Static);
var parameters = methodInfo.GetParameters();
Assert.AreEqual(1, parameters.Length, "Test command should have one parameter");
// Act - Try to get the CliArg attribute from the parameter
var parameter = parameters[0];
var attribute = parameter.GetCustomAttribute<CliArgAttribute>();
// Assert - Parameter attribute should be present and contain expected data
Assert.IsNotNull(attribute, "Parameter should have CliArg attribute");
Assert.AreEqual("message", attribute.Name, "Parameter name should match attribute");
Assert.AreEqual("The message to log", attribute.Description,
"Parameter description should match attribute");
Assert.IsTrue(attribute.Required, "Parameter should be marked as required");
}
[Test]
public void CommandRegistry_DiscoverCommands_FindsRegisteredCommands()
{
// Arrange & Act - Discover all commands via CommandRegistry
var commands = CommandRegistry.DiscoverCommands();
// Assert - Should find our test command
Assert.IsNotNull(commands, "Command discovery should return a collection");
Assert.Greater(commands.Count(), 0, "Should discover at least one command");
// Find our specific test command
var testCommand = commands.FirstOrDefault(cmd => cmd.Name == "log_editor");
Assert.IsNotNull(testCommand, "Should discover the log_editor test command");
Assert.AreEqual("Log a message to Unity Editor console", testCommand.Description);
Assert.IsTrue(testCommand.MainThreadRequired, "Test command should require main thread");
// Verify parameter information
Assert.AreEqual(1, testCommand.Parameters.Count, "Command should have one parameter");
var messageParam = testCommand.Parameters.First();
Assert.AreEqual("message", messageParam.Name);
Assert.AreEqual("The message to log", messageParam.Description);
Assert.IsTrue(messageParam.Required);
Assert.AreEqual(typeof(string), messageParam.ParameterType);
}
[Test]
public void CommandRegistry_DiscoverCommands_PopulatesRuntimeOnlyFlag()
{
// Arrange & Act
var commands = CommandRegistry.DiscoverCommands().ToList();
// Assert - runtime commands are tagged RuntimeOnly, editor commands are not
var editorStatus = commands.First(c => c.Name == "editor_status");
var runtimeStatus = commands.First(c => c.Name == "runtime_status");
Assert.IsTrue(runtimeStatus.RuntimeOnly, "eval should be marked RuntimeOnly");
Assert.IsFalse(editorStatus.RuntimeOnly, "editor_status should not be RuntimeOnly");
}
[Test]
public void JsonSchemaGeneration_FromCommandInfo_CreatesValidSchema()
{
// Arrange - Get our test command
var commands = CommandRegistry.DiscoverCommands();
var testCommand = commands.First(cmd => cmd.Name == "log_editor");
// Act - Generate JSON schema for the command
var schema = JsonSchemaGenerator.GenerateCommandSchema(testCommand);
// Assert - Schema should be valid JSON Schema format
Assert.IsNotNull(schema, "Schema should not be null");
// Parse as JSON to verify structure
var schemaJson = JObject.Parse(schema);
// Verify root schema properties
Assert.AreEqual("object", schemaJson["type"]?.ToString());
Assert.IsNotNull(schemaJson["properties"], "Schema should have properties section");
Assert.IsNotNull(schemaJson["required"], "Schema should have required array");
// Verify command metadata
Assert.AreEqual("log_editor", schemaJson["title"]?.ToString());
Assert.AreEqual("Log a message to Unity Editor console", schemaJson["description"]?.ToString());
// Verify parameter schema
var properties = schemaJson["properties"] as JObject;
Assert.IsNotNull(properties["message"], "Should have message parameter");
var messageProperty = properties["message"] as JObject;
Assert.AreEqual("string", messageProperty["type"]?.ToString());
Assert.AreEqual("The message to log", messageProperty["description"]?.ToString());
// Verify required array
var required = schemaJson["required"] as JArray;
Assert.Contains("message", required.ToObject<string[]>());
}
[Test]
public void JsonSchemaGeneration_MultipleParameterTypes_MapsTypesCorrectly()
{
// Arrange - Get test command with multiple parameter types
var commands = CommandRegistry.DiscoverCommands();
var testCommand = commands.First(cmd => cmd.Name == "test_types");
// Act - Generate schema
var schema = JsonSchemaGenerator.GenerateCommandSchema(testCommand);
var schemaJson = JObject.Parse(schema);
// Assert - Verify different parameter types are mapped correctly
var properties = schemaJson["properties"] as JObject;
// Integer parameter
var countProperty = properties["count"] as JObject;
Assert.AreEqual("integer", countProperty["type"]?.ToString());
Assert.AreEqual(1, countProperty["default"]?.ToObject<int>());
// Boolean parameter
var enabledProperty = properties["enabled"] as JObject;
Assert.AreEqual("boolean", enabledProperty["type"]?.ToString());
Assert.AreEqual(false, enabledProperty["default"]?.ToObject<bool>());
// Float parameter
var factorProperty = properties["factor"] as JObject;
Assert.AreEqual("number", factorProperty["type"]?.ToString());
Assert.AreEqual(1.0f, factorProperty["default"]?.ToObject<float>());
// Verify no required parameters (all have defaults)
var required = schemaJson["required"] as JArray;
Assert.AreEqual(0, required.Count, "All parameters should be optional with default values");
}
[Test]
public void JsonSchemaGeneration_StructuredObjectParameter_EmitsNestedObjectSchema()
{
// Arrange - command whose single parameter is an IStructuredCommandInput DTO
var commands = CommandRegistry.DiscoverCommands();
var testCommand = commands.First(cmd => cmd.Name == "test_structured");
// Act
var schemaJson = JObject.Parse(JsonSchemaGenerator.GenerateCommandSchema(testCommand));
// Assert - the payload param is a real nested object schema, not a "string" fallback
var payload = schemaJson["properties"]?["payload"] as JObject;
Assert.IsNotNull(payload, "payload parameter should be present");
Assert.AreEqual("object", payload["type"]?.ToString(), "structured param should map to object");
Assert.AreEqual(false, payload["additionalProperties"]?.ToObject<bool>());
var props = payload["properties"] as JObject;
Assert.IsNotNull(props, "nested object should expose its own properties");
// Scalar member with description from [CliArg]
Assert.AreEqual("string", props["name"]?["type"]?.ToString());
Assert.AreEqual("Display name", props["name"]?["description"]?.ToString());
Assert.AreEqual("integer", props["count"]?["type"]?.ToString());
// Array member emits items schema
Assert.AreEqual("array", props["tags"]?["type"]?.ToString());
Assert.AreEqual("string", props["tags"]?["items"]?["type"]?.ToString());
// Nested structured member recurses
var nested = props["nested"] as JObject;
Assert.IsNotNull(nested, "nested structured member should be present");
Assert.AreEqual("object", nested["type"]?.ToString());
Assert.AreEqual("boolean", nested["properties"]?["enabled"]?["type"]?.ToString());
// Required propagates from [CliArg(Required = true)] on a member
var required = payload["required"]?.ToObject<string[]>();
Assert.IsNotNull(required, "structured object should have a required array");
CollectionAssert.Contains(required, "name");
}
[Test]
public void EditorStatusCommand_IsDiscovered_ByCommandRegistry()
{
// Arrange & Act - Discover all commands
var commands = CommandRegistry.DiscoverCommands();
// Assert - Should find the editor_status command
var editorStatusCommand = commands.FirstOrDefault(cmd => cmd.Name == "editor_status");
Assert.IsNotNull(editorStatusCommand, "Should discover the editor_status command");
Assert.AreEqual("Get detailed Unity Editor status and state information", editorStatusCommand.Description);
Assert.IsTrue(editorStatusCommand.MainThreadRequired, "editor_status should require main thread");
// Verify it has no parameters (editor status doesn't need input)
Assert.AreEqual(0, editorStatusCommand.Parameters.Count, "editor_status should have no parameters");
}
[Test]
public void EditorStatusCommand_DirectExecution_WorksCorrectly()
{
// Arrange & Act - Execute editor_status command directly (bypass all HTTP and threading)
try
{
var result = Unity.Pipeline.Editor.Commands.EditorStatusCommand.GetEditorStatus();
// Assert
Assert.IsNotNull(result, "editor_status command should return a result");
Assert.IsInstanceOf<Unity.Pipeline.Models.StatusResponse>(result, "Should return StatusResponse type");
var status = result as Unity.Pipeline.Models.StatusResponse;
Assert.IsNotNull(status.Status, "Status should have Status field");
Assert.IsNotNull(status.UnityVersion, "Status should have UnityVersion field");
}
catch (System.Exception)
{
throw;
}
}
}
/// <summary>
/// Test commands for verifying command registration attributes.
/// </summary>
public static class DummyCommands
{
// Private on purpose: verifies the registry can discover and execute
// non-public static methods (see CommandRegistry.CreateCommandInfo).
[CliCommand("log_editor", "Log a message to Unity Editor console")]
private static void TestLogCommand([CliArg("message", "The message to log", Required = true)] string message)
{
UnityEngine.Debug.Log(message);
}
[CliCommand("test_types", "Test command with various parameter types")]
public static void TestTypesCommand(
[CliArg("count", "Number of items")] int count = 1,
[CliArg("enabled", "Whether feature is enabled")] bool enabled = false,
[CliArg("factor", "Multiplier factor")] float factor = 1.0f)
{
// Test command with various types
}
[CliCommand("test_structured", "Test command with a structured object parameter")]
public static void TestStructuredCommand(
[CliArg("payload", "Structured payload")] SampleStructuredInput payload = null)
{
// Test command with a nested-object parameter
}
}
/// <summary>Structured-input DTO used to verify nested object schema generation.</summary>
public class SampleStructuredInput : IStructuredCommandInput
{
[CliArg("name", "Display name", Required = true)]
public string Name { get; set; }
[CliArg("count", "How many")]
public int Count { get; set; }
[CliArg("tags", "Associated tags")]
public string[] Tags { get; set; }
[CliArg("nested", "Nested child object")]
public SampleNestedInput Nested { get; set; }
}
public class SampleNestedInput : IStructuredCommandInput
{
[CliArg("enabled", "Toggle")]
public bool Enabled { get; set; }
}
}