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,187 @@
using System.Collections;
using NUnit.Framework;
using Unity.Pipeline.Editor;
using Unity.Pipeline.Tests.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
namespace Unity.Pipeline.Tests.Editor.ServerLifecyle
{
/// <summary>
/// Tests for simultaneous operation of Editor and Runtime servers.
/// Validates port range separation and dual server functionality.
///
/// Marked [Explicit]: starts a real EditorPipelineServer + RuntimePipelineManager, which
/// conflict with the live editor server present in every editor session. Excluded from the
/// normal/dogfood run; run deliberately (e.g. Test Runner window). See [[GlobalServerStateGuard]].
/// </summary>
[Explicit("Server-lifecycle test; conflicts with the live editor server. Run deliberately.")]
[Category("ServerLifecycle")]
public class DualServerTests
{
private GameObject m_TestGameObject;
private RuntimePipelineManager m_RuntimeManager;
private EditorPipelineServer m_EditorServer;
[SetUp]
public void SetUp()
{
// RuntimePipelineManager + EditorPipelineServer here mutate global state shared with
// the live editor server (descriptor file, command discovery).
GlobalServerStateGuard.Capture();
// Create test GameObject with RuntimePipelineManager
m_TestGameObject = new GameObject("TestRuntimePipelineManager");
m_RuntimeManager = m_TestGameObject.AddComponent<RuntimePipelineManager>();
// Configure for testing
m_RuntimeManager.enableInBuilds = true;
// Create Editor server for dual-server tests
m_EditorServer = new EditorPipelineServer();
}
[TearDown]
public void TearDown()
{
// Stop servers
if (m_RuntimeManager != null && m_RuntimeManager.IsServerRunning)
{
m_RuntimeManager.StopServer();
}
if (m_EditorServer != null && m_EditorServer.IsRunning)
{
m_EditorServer.Stop();
}
// Clean up GameObjects
if (m_TestGameObject != null)
{
Object.DestroyImmediate(m_TestGameObject);
}
// Restore global state the servers/manager mutated (runs last, after our own cleanup).
GlobalServerStateGuard.Restore();
}
[UnityTest]
public IEnumerator DualServers_EditorAndRuntime_UsesDifferentPortRanges()
{
// Act - Start Editor server first
m_EditorServer.Start();
yield return null;
// Start Runtime server
m_RuntimeManager.StartServer();
yield return null;
// Assert - Both servers should be running on different port ranges
Assert.IsTrue(m_EditorServer.IsRunning, "Editor server should be running");
Assert.IsTrue(m_RuntimeManager.IsServerRunning, "Runtime server should be running");
// Editor should use 7800-7899
Assert.GreaterOrEqual(m_EditorServer.Port, 7800, "Editor server should use port >= 7800");
Assert.LessOrEqual(m_EditorServer.Port, 7899, "Editor server should use port <= 7899");
// Runtime should use 7900-7999
Assert.GreaterOrEqual(m_RuntimeManager.ActualPort, 7900, "Runtime server should use port >= 7900");
Assert.LessOrEqual(m_RuntimeManager.ActualPort, 7999, "Runtime server should use port <= 7999");
// Ports should not conflict
Assert.AreNotEqual(m_EditorServer.Port, m_RuntimeManager.ActualPort, "Editor and Runtime servers should use different ports");
}
[UnityTest]
public IEnumerator DualServers_BothStatusEndpoints_ReturnValidResponses()
{
// Arrange - Start both servers
m_EditorServer.Start();
m_RuntimeManager.StartServer();
yield return null;
if (!m_EditorServer.IsRunning || !m_RuntimeManager.IsServerRunning)
{
Assert.Fail("Both servers should be running for this test");
yield break;
}
// Test Editor server status endpoint
PipelineResponse editorResponse = null;
using (var editorClient = new PipelineClient(m_EditorServer))
yield return editorClient.GetAsync("/api/status", r => editorResponse = r);
Assert.IsTrue(editorResponse.IsSuccess, $"Editor status endpoint should work: {editorResponse.Error}");
Assert.IsNotEmpty(editorResponse.RawResponse, "Editor status response should not be empty");
// Test Runtime server status endpoint
PipelineResponse runtimeResponse = null;
using (var runtimeClient = new PipelineClient(m_RuntimeManager))
yield return runtimeClient.GetAsync("/api/status", r => runtimeResponse = r);
Assert.IsTrue(runtimeResponse.IsSuccess, $"Runtime status endpoint should work: {runtimeResponse.Error}");
Assert.IsNotEmpty(runtimeResponse.RawResponse, "Runtime status response should not be empty");
}
[UnityTest]
public IEnumerator DualServers_Authentication_EachRequiresOwnToken()
{
// Arrange - Start both servers
m_EditorServer.Start();
m_RuntimeManager.StartServer();
yield return null;
if (!m_EditorServer.IsRunning || !m_RuntimeManager.IsServerRunning)
{
Assert.Fail("Both servers should be running for this test");
yield break;
}
// The runtime client authenticates with the server's own token; a request should be
// accepted (not 401). (Rejection of missing/invalid tokens is covered by ServerAuthTests.)
PipelineResponse runtimeResponse = null;
using (var runtimeClient = new PipelineClient(m_RuntimeManager))
yield return runtimeClient.GetAsync("/api/status", r => runtimeResponse = r);
Assert.AreNotEqual(401, runtimeResponse.StatusCode, "Runtime server should accept its own token");
}
[UnityTest]
public IEnumerator DualServers_StartStopSequence_WorksCorrectly()
{
// Test various start/stop sequences to ensure no interference
// Sequence 1: Start Editor first
m_EditorServer.Start();
yield return new WaitForSeconds(0.1f);
m_RuntimeManager.StartServer();
yield return new WaitForSeconds(0.1f);
Assert.IsTrue(m_EditorServer.IsRunning, "Editor should be running");
Assert.IsTrue(m_RuntimeManager.IsServerRunning, "Runtime should be running");
// Stop both
m_EditorServer.Stop();
m_RuntimeManager.StopServer();
yield return new WaitForSeconds(0.1f);
Assert.IsFalse(m_EditorServer.IsRunning, "Editor should be stopped");
Assert.IsFalse(m_RuntimeManager.IsServerRunning, "Runtime should be stopped");
// Sequence 2: Start Runtime first
m_RuntimeManager.StartServer();
yield return new WaitForSeconds(0.1f);
m_EditorServer.Start();
yield return new WaitForSeconds(0.1f);
Assert.IsTrue(m_EditorServer.IsRunning, "Editor should be running");
Assert.IsTrue(m_RuntimeManager.IsServerRunning, "Runtime should be running");
// Should still use correct port ranges regardless of start order
Assert.GreaterOrEqual(m_EditorServer.Port, 7800, "Editor should use Editor range");
Assert.LessOrEqual(m_EditorServer.Port, 7899, "Editor should use Editor range");
Assert.GreaterOrEqual(m_RuntimeManager.ActualPort, 7900, "Runtime should use Runtime range");
Assert.LessOrEqual(m_RuntimeManager.ActualPort, 7999, "Runtime should use Runtime range");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb4e86ff6994ad141ae2cc0f129206e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,124 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Editor;
using Unity.Pipeline.Models;
using Newtonsoft.Json.Linq;
using UnityEngine;
using System.IO;
using Unity.Pipeline.Tests.Editor;
using Unity.Pipeline.Tests.Runtime;
namespace Unity.Pipeline.Tests.Editor.ServerLifecyle
{
/// <summary>
/// End-to-end tests demonstrating complete Pipeline workflow.
/// These test the full roundtrip: discovery → connection → command execution
/// that simulates real CLI usage patterns.
///
/// Marked [Explicit]: starts its own EditorPipelineServer (and manages the shared descriptor),
/// which conflicts with the live editor server. Excluded from the normal/dogfood run.
/// TODO: to rejoin the dogfood run, refactor to drive an isolated TestEditorPipelineServer
/// (ports 7850+, WritesDescriptor=false) instead of starting a real server.
/// </summary>
[Explicit("Starts its own server; conflicts with the live editor server. Run deliberately.")]
[Category("ServerLifecycle")]
public class EndToEndTests
{
[Test]
public async Task CompleteWorkflow_DiscoverConnectExecute_WorksEndToEnd()
{
// Setup
CommandRegistry.SetDiscovery(new TypeCacheCommandDiscovery());
// Step 1: Start Pipeline Server (simulates Editor startup)
var server = new EditorPipelineServer();
server.Start();
try
{
var projectPath = Path.GetDirectoryName(Application.dataPath);
// Step 2: CLI Discovery - Find running instances
var instances = MockCliDiscovery.DiscoverInstances(new[] { projectPath });
var runningInstances = instances.Where(i => i.IsRunning).ToList();
Assert.Greater(runningInstances.Count, 0, "Should discover at least one running instance");
var targetInstance = runningInstances.First();
// Give server a moment to fully start
await Task.Delay(100);
// Step 3: CLI Connection Validation
var isReachable = await MockCliDiscovery.ValidateConnection(targetInstance.Descriptor);
Assert.IsTrue(isReachable, "Discovered instance should be reachable");
// Step 4: CLI Command Discovery - Get available commands
using (var client = new PipelineClient(server))
{
var commandsResponse = await client.GetAsync("/api/commands");
Assert.IsTrue(commandsResponse.IsSuccess, "Should be able to get commands list");
var commands = commandsResponse.JsonResponse["commands"] as JArray;
Assert.Greater(commands.Count, 0, "Should have available commands");
// Step 5: CLI Command Execution - Execute log_editor command
var execResponse = await client.ExecuteCommandAsync(
"log_editor", new { message = "Hello from end-to-end test!" });
Assert.IsTrue(execResponse.IsSuccess,
$"Command execution should succeed. Response: {execResponse.RawResponse}");
var execData = execResponse.JsonResponse;
Assert.IsTrue(execData["success"].ToObject<bool>(), "Command should execute successfully");
Assert.AreEqual("log_editor", execData["command"]?.ToString());
}
}
finally
{
server.Stop();
}
}
[Test]
public async Task RealWorldScenario_MultipleCommands_ExecutesSequentially()
{
// Setup
CommandRegistry.SetDiscovery(new TypeCacheCommandDiscovery());
var server = new EditorPipelineServer();
server.Start();
try
{
using (var client = new PipelineClient(server))
{
// Simulate CLI batch execution of multiple commands
var commands = new CommandExecutionRequest[]
{
new CommandExecutionRequest("log_editor") { Parameters = { ["message"] = "Starting batch execution" } },
new CommandExecutionRequest("test_types") { Parameters = { ["count"] = 5, ["enabled"] = true, ["factor"] = 2.5f } },
new CommandExecutionRequest("log_editor") { Parameters = { ["message"] = "Batch execution completed" } }
};
foreach (var cmd in commands)
{
var response = await client.ExecuteCommandAsync(cmd.Command, cmd.Parameters);
Assert.IsTrue(response.IsSuccess,
$"Command '{cmd.Command}' should execute successfully. Response: {response.RawResponse}");
Assert.IsTrue(response.JsonResponse["success"].ToObject<bool>(),
$"Command '{cmd.Command}' should succeed");
}
}
}
finally
{
server.Stop();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a97f33bd666ad6340965b07f15094045
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
using System.IO;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Editor;
using Unity.Pipeline.Models;
using UnityEngine;
namespace Unity.Pipeline.Tests.Editor.ServerLifecyle
{
/// <summary>
/// Isolates server-lifecycle tests from the live editor pipeline server, which shares some global
/// state in-process. Call Capture() in [SetUp] and Restore() in [TearDown].
///
/// It records whether a live server was advertised before the test and restores command discovery
/// (RuntimePipelineManager.Awake switches it to reflection). The main-thread dispatcher is
/// per-server now, so there is nothing global to swap.
/// </summary>
public static class GlobalServerStateGuard
{
static string s_DescriptorPath;
static bool s_HadLiveServer;
public static void Capture()
{
var projectPath = Path.GetDirectoryName(Application.dataPath);
s_DescriptorPath = InstanceDescriptor.GetDescriptorFilePath(projectPath);
// Key off the live server OBJECT, not the descriptor file: a prior test may have left the
// live server running while the shared descriptor was already deleted (object/file desync).
// Keying off the file would then wrongly skip the restart below.
s_HadLiveServer = PipelineServerStartup.Server != null;
}
public static void Restore()
{
// A server-lifecycle test starts/stops its own EditorPipelineServer on the live port,
// which tears down the live listener and deletes the shared `.unity-pipeline-port`
// descriptor. Rewriting the descriptor file is NOT enough — it would advertise a port
// nothing listens on (the symptom: menu shows "running" but the server is unreachable).
// Fully restart the live server so the listener and descriptor are consistent (this also
// re-arms the watchdog and auto-tick). Mirrors RuntimePipelineServerTests.TearDown.
try
{
if (s_HadLiveServer)
PipelineServerStartup.RestartServer();
else if (s_DescriptorPath != null && File.Exists(s_DescriptorPath))
File.Delete(s_DescriptorPath); // No live server before the test — clear any stray descriptor.
}
catch { /* best effort */ }
// RuntimePipelineManager.Awake switches discovery to reflection; restore TypeCache.
CommandRegistry.SetDiscovery(new TypeCacheCommandDiscovery());
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a715663d547625346836df81c2aee9a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
using System.IO;
using System.Threading.Tasks;
using NUnit.Framework;
using Unity.Pipeline.Editor;
using Unity.Pipeline.Models;
using Unity.Pipeline.Tests.Runtime;
using UnityEngine;
namespace Unity.Pipeline.Tests.Editor.ServerLifecyle
{
/// <summary>
/// Regression test for the full-suite run leaving the live editor server unreachable.
///
/// <see cref="GlobalServerStateGuard.Restore"/> is the safety net that server-lifecycle tests
/// rely on (in [TearDown]) to hand the live editor server back intact. Its contract is not "the
/// descriptor file is present" but "the live server is actually running and reachable". A guard
/// that only rewrites the descriptor file leaves it advertising a port nothing listens on —
/// which is exactly how the suite used to finish with the server "running" in the menu but
/// unreachable.
/// </summary>
[Explicit("Manipulates the live editor server; run deliberately (e.g. from the Test Runner window).")]
[Category("ServerLifecycle")]
public class GlobalServerStateGuardTests
{
[TearDown]
public void TearDown()
{
// Safety net: never leave the live server down for the next test/dogfood command, even if
// an assertion above failed before the guard could restore it.
PipelineServerStartup.EnsureServerStarted();
}
[Test]
public async Task Restore_AfterLiveListenerTornDown_LeavesServerReachable()
{
// Arrange: a live server is running and advertised (as in every interactive editor).
PipelineServerStartup.EnsureServerStarted();
Assert.IsNotNull(PipelineServerStartup.Server, "Precondition: a live server should exist");
Assert.IsTrue(PipelineServerStartup.Server.IsRunning, "Precondition: live server should be running");
GlobalServerStateGuard.Capture();
// Simulate a server-lifecycle test that tears down the live listener and deletes the
// shared descriptor (what new EditorPipelineServer().Start()/Stop() does on the live port).
PipelineServerStartup.StopServer();
var projectRoot = Path.GetDirectoryName(Application.dataPath);
Assert.IsNull(InstanceDescriptor.ReadFromProjectRoot(projectRoot),
"Sanity: descriptor should be gone after the live server stopped");
// Act
GlobalServerStateGuard.Restore();
// Assert: the guard must leave a live, reachable server — not just a descriptor file.
var server = PipelineServerStartup.Server;
Assert.IsNotNull(server, "Restore() must leave a live server running");
Assert.IsTrue(server.IsRunning, "Restore() must leave the live server running");
var descriptor = InstanceDescriptor.ReadFromProjectRoot(projectRoot);
Assert.IsNotNull(descriptor, "Restore() must leave a discovery descriptor");
Assert.AreEqual(server.Port, descriptor.Port,
"Descriptor port must match the running server's port");
using (var client = new PipelineClient($"http://localhost:{descriptor.Port}", descriptor.EvalToken))
{
var status = await client.GetStatusAsync();
Assert.IsTrue(status.IsSuccess,
"Live server must answer /api/status after Restore() — the descriptor must point at a live listener");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c16e03783027c2a48bf19735c2b8823d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,131 @@
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using System.IO;
using System.Threading.Tasks;
using Unity.Pipeline.Editor;
using Unity.Pipeline.Models;
using Unity.Pipeline.Tests.Runtime;
using UnityEngine;
namespace Unity.Pipeline.Tests.Editor.ServerLifecyle
{
/// <summary>
/// Tests for PipelineServer lifecycle and basic HTTP functionality.
/// These test the public interface behaviors that CLI tools will rely on.
///
/// Marked [Explicit]: they start/stop a real EditorPipelineServer and manage the shared
/// `.unity-pipeline-port` descriptor, which conflicts with the live editor server that exists
/// in every editor session. They are excluded from the normal/dogfood run and meant to be run
/// deliberately (e.g. from the Test Runner window). See [[GlobalServerStateGuard]].
/// </summary>
[Explicit("Server-lifecycle test; conflicts with the live editor server. Run deliberately.")]
[Category("ServerLifecycle")]
public class PipelineServerTests
{
// These tests start/stop their own EditorPipelineServer, which writes/deletes the shared
// `.unity-pipeline-port` descriptor and would corrupt the live editor server. Guard it.
[SetUp]
public void SetUp() => GlobalServerStateGuard.Capture();
[TearDown]
public void TearDown() => GlobalServerStateGuard.Restore();
[Test]
public void ServerLifecycle_StartAndStop_ManagesRunningState()
{
// Arrange
var server = new EditorPipelineServer();
// Assert initial state
Assert.IsFalse(server.IsRunning, "Server should not be running initially");
Assert.AreEqual(0, server.Port, "Port should be 0 when not running");
// Act - Start server
server.Start();
// Assert - Started state
Assert.IsTrue(server.IsRunning, "Server should be running after Start()");
Assert.Greater(server.Port, 0, "Port should be assigned after Start()");
Assert.GreaterOrEqual(server.Port, 7800, "Port should be in range 7800-7899");
Assert.LessOrEqual(server.Port, 7899, "Port should be in range 7800-7899");
// Act - Stop server
server.Stop();
// Assert - Stopped state
Assert.IsFalse(server.IsRunning, "Server should not be running after Stop()");
// Note: Port may retain value after stop for diagnostic purposes
}
[Test]
public async Task StatusEndpoint_GetApiStatus_ReturnsBasicServerInfo()
{
// Arrange
var server = new EditorPipelineServer();
server.Start();
try
{
using (var client = new PipelineClient(server))
{
// Act - Call /api/status endpoint (basic server health)
var response = await client.GetHttpAsync("/api/status");
var jsonContent = await response.Content.ReadAsStringAsync();
// Assert - Response structure
Assert.IsTrue(response.IsSuccessStatusCode,
$"Basic status endpoint should return success, got: {response.StatusCode}");
Assert.AreEqual("application/json", response.Content.Headers.ContentType.MediaType,
"Status endpoint should return JSON content type");
// Assert - Basic server status JSON structure (not StatusResponse)
var statusJson = JObject.Parse(jsonContent);
Assert.IsNotNull(statusJson, "Should be able to parse basic status JSON");
// Verify basic server fields
Assert.AreEqual("ready", statusJson["status"]?.ToString(), "Server status should be ready");
}
}
finally
{
server.Stop();
}
}
[Test]
public void InstanceDescriptor_ServerStartStop_ManagesDescriptorFile()
{
// Arrange
var server = new EditorPipelineServer();
var projectPath = Path.GetDirectoryName(Application.dataPath);
var descriptorFilePath = InstanceDescriptor.GetDescriptorFilePath(projectPath);
// Ensure no existing descriptor file
if (File.Exists(descriptorFilePath))
{
File.Delete(descriptorFilePath);
}
// Act - Start server
server.Start();
// Assert - Descriptor file created
Assert.IsTrue(File.Exists(descriptorFilePath),
"Instance descriptor file should be created when server starts");
// Read and verify descriptor content
var descriptor = InstanceDescriptor.ReadFromProjectRoot(projectPath);
Assert.IsNotNull(descriptor, "Should be able to read descriptor file");
Assert.AreEqual(server.Port, descriptor.Port, "Descriptor should contain correct port");
Assert.Greater(descriptor.Pid, 0, "Descriptor should contain valid PID");
Assert.IsNotEmpty(descriptor.ProjectPath, "Descriptor should contain project path");
// Act - Stop server
server.Stop();
// Assert - Descriptor file removed
Assert.IsFalse(File.Exists(descriptorFilePath),
"Instance descriptor file should be removed when server stops");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42319be0b075849479314f25fe300c69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: