Add Unity Pipeline package support
This commit is contained in:
+448
@@ -0,0 +1,448 @@
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Unity.Pipeline.Commands;
|
||||
using Unity.Pipeline.HotReload;
|
||||
using Unity.Pipeline.Runtime.Commands;
|
||||
using Unity.Pipeline.Security;
|
||||
using Unity.Pipeline.Tests;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration tests for hot reload CLI commands through the Pipeline Server.
|
||||
/// Tests the complete workflow: CLI commands → Pipeline Server → Hot Reload Compilation → Registry.
|
||||
/// </summary>
|
||||
[Ignore("HotReload in-place CLI workflow is deferred until the autonomous test loop is solid; it exercises the known transformation bug and starts its own server. Re-enable when revisiting in-place reload.")]
|
||||
public class HotReloadCliIntegrationTests
|
||||
{
|
||||
private GameObject testGameObject;
|
||||
private RuntimePipelineManager pipelineManager;
|
||||
private string validToken;
|
||||
private string testHotReloadDir;
|
||||
private PipelineClient pipelineClient;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
// Setup pipeline server
|
||||
testGameObject = new GameObject("TestHotReloadPipelineManager");
|
||||
pipelineManager = testGameObject.AddComponent<RuntimePipelineManager>();
|
||||
|
||||
// Configure for testing
|
||||
pipelineManager.enableInBuilds = true;
|
||||
|
||||
// Get security token
|
||||
SecurityTokenManager.ClearCache();
|
||||
validToken = SecurityTokenManager.GetOrCreateToken();
|
||||
|
||||
// Setup hot reload test files
|
||||
testHotReloadDir = Unity.Pipeline.Compilation.HotReloadCompiler.GetHotReloadPath("CliTests");
|
||||
Directory.CreateDirectory(testHotReloadDir);
|
||||
|
||||
// Clear registry
|
||||
HotReloadRegistry.ClearAllForTesting();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Stop server and cleanup client
|
||||
pipelineClient?.Dispose();
|
||||
if (pipelineManager?.IsServerRunning == true)
|
||||
{
|
||||
pipelineManager.StopServer();
|
||||
}
|
||||
|
||||
// Cleanup game objects
|
||||
if (testGameObject != null)
|
||||
{
|
||||
Object.DestroyImmediate(testGameObject);
|
||||
}
|
||||
|
||||
// Cleanup test files
|
||||
if (Directory.Exists(testHotReloadDir))
|
||||
{
|
||||
Directory.Delete(testHotReloadDir, true);
|
||||
}
|
||||
|
||||
// Clear state
|
||||
HotReloadRegistry.ClearAllForTesting();
|
||||
SecurityTokenManager.ClearCache();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator CLI_ReloadFile_EndToEndWorkflow()
|
||||
{
|
||||
// Arrange - Create a valid hot reload file
|
||||
var testFileName = "PlayerMovement.cs";
|
||||
var testFilePath = Path.Combine(testHotReloadDir, testFileName);
|
||||
File.WriteAllText(testFilePath, @"
|
||||
using Unity.Pipeline.HotReload;
|
||||
using UnityEngine;
|
||||
|
||||
// Target class with reloadable method
|
||||
public class PlayerController
|
||||
{
|
||||
public float speed = 5.0f;
|
||||
public Vector3 position = Vector3.zero;
|
||||
|
||||
[HotReloadWithOverrides(Id = ""PlayerController.Move"")]
|
||||
public void Move()
|
||||
{
|
||||
position += Vector3.forward * speed * Time.deltaTime;
|
||||
Debug.Log($""Original Move: position = {position}"");
|
||||
}
|
||||
}
|
||||
|
||||
// Hot reload override
|
||||
public static class PlayerMovementHotReload
|
||||
{
|
||||
[HotReloadOverrideMethod(""PlayerController.Move"")]
|
||||
public static void Move(PlayerController instance)
|
||||
{
|
||||
// Hot reload version: move faster and add rotation
|
||||
instance.position += Vector3.forward * instance.speed * 2.0f * Time.deltaTime;
|
||||
Debug.Log($""Hot Reload Move: position = {instance.position} (2x speed!)"");
|
||||
}
|
||||
}");
|
||||
|
||||
// Start server
|
||||
pipelineManager.StartServer();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
Assert.IsTrue(pipelineManager.IsServerRunning, "Server should be running");
|
||||
|
||||
// Create client
|
||||
pipelineClient = new PipelineClient($"http://localhost:{pipelineManager.ActualPort}", validToken);
|
||||
|
||||
// Act - Execute reload_file_override command via CLI using coroutine
|
||||
PipelineResponse response = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("reload_file_override", new
|
||||
{
|
||||
filename = $"CliTests/{testFileName}",
|
||||
token = validToken,
|
||||
timeout = 10000
|
||||
}, r => response = r);
|
||||
|
||||
// Assert - Should succeed
|
||||
Assert.IsNotNull(response, "Should get response from CLI command");
|
||||
Assert.IsTrue(response.IsSuccess, $"HTTP request should succeed. Error: {response.Error}");
|
||||
Assert.IsTrue(response.IsCommandSuccess, $"CLI command should succeed. Error: {response.CommandError}");
|
||||
|
||||
// Verify registry state
|
||||
var stats = HotReloadRegistry.GetStats();
|
||||
Assert.Greater(stats.ActiveOverrideCount, 0, "Should have active overrides registered");
|
||||
Assert.Greater(stats.ReloadableMethodCount, 0, "Should have reloadable methods registered");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator CLI_HotReloadStatus_ReturnsCorrectStats()
|
||||
{
|
||||
// Start server
|
||||
pipelineManager.StartServer();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
pipelineClient = new PipelineClient($"http://localhost:{pipelineManager.ActualPort}", validToken);
|
||||
|
||||
// Get initial status
|
||||
PipelineResponse initialResponse = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("hotreload_status", new
|
||||
{
|
||||
token = validToken
|
||||
}, r => initialResponse = r);
|
||||
|
||||
Assert.IsTrue(initialResponse.IsCommandSuccess, "Initial status should succeed");
|
||||
|
||||
// Get message from nested result object
|
||||
var initialResult = initialResponse.JsonResponse["result"];
|
||||
var initialMessage = initialResult?["message"]?.ToString() ?? "";
|
||||
|
||||
// Load a hot reload file first
|
||||
var testFile = Path.Combine(testHotReloadDir, "StatusTest.cs");
|
||||
File.WriteAllText(testFile, @"
|
||||
using Unity.Pipeline.HotReload;
|
||||
using UnityEngine;
|
||||
|
||||
public class TestComponent
|
||||
{
|
||||
[HotReloadWithOverrides(Id = ""TestComponent.Update"")]
|
||||
public void Update() => Debug.Log(""Original Update"");
|
||||
}
|
||||
|
||||
public static class TestHotReload
|
||||
{
|
||||
[HotReloadOverrideMethod(""TestComponent.Update"")]
|
||||
public static void Update(TestComponent instance) => Debug.Log(""Hot Reload Update"");
|
||||
}");
|
||||
|
||||
// Load the file
|
||||
PipelineResponse loadResponse = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("reload_file_override", new
|
||||
{
|
||||
filename = "CliTests/StatusTest.cs",
|
||||
token = validToken
|
||||
}, r => loadResponse = r);
|
||||
|
||||
Assert.IsTrue(loadResponse.IsCommandSuccess, "File load should succeed");
|
||||
|
||||
// Get updated status
|
||||
PipelineResponse finalResponse = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("hotreload_status", new
|
||||
{
|
||||
token = validToken
|
||||
}, r => finalResponse = r);
|
||||
|
||||
Assert.IsTrue(finalResponse.IsCommandSuccess, "Final status should succeed");
|
||||
|
||||
// Get message from nested result object
|
||||
var finalResult = finalResponse.JsonResponse["result"];
|
||||
var finalMessage = finalResult?["message"]?.ToString() ?? "";
|
||||
Assert.IsNotEmpty(finalMessage, "Should have status message");
|
||||
Assert.That(finalMessage, Does.Contain("Active Overrides: 1"), "Should show 1 active override");
|
||||
Assert.That(finalMessage, Does.Contain("Reloadable Methods: 1"), "Should show 1 reloadable method");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator CLI_CleanupHotReload_ClearsRegistry()
|
||||
{
|
||||
// Start server and load a hot reload file
|
||||
pipelineManager.StartServer();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
pipelineClient = new PipelineClient($"http://localhost:{pipelineManager.ActualPort}", validToken);
|
||||
|
||||
// Load hot reload file to create some state
|
||||
var testFile = Path.Combine(testHotReloadDir, "CleanupTest.cs");
|
||||
File.WriteAllText(testFile, @"
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class CleanupTestClass
|
||||
{
|
||||
[HotReloadWithOverrides(Id = ""CleanupTest.Method"")]
|
||||
public void TestMethod() { }
|
||||
}
|
||||
|
||||
public static class CleanupTestHotReload
|
||||
{
|
||||
[HotReloadOverrideMethod(""CleanupTest.Method"")]
|
||||
public static void TestMethod(CleanupTestClass instance) { }
|
||||
}");
|
||||
|
||||
PipelineResponse loadResponse = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("reload_file_override", new
|
||||
{
|
||||
filename = "CliTests/CleanupTest.cs",
|
||||
token = validToken
|
||||
}, r => loadResponse = r);
|
||||
|
||||
Assert.IsTrue(loadResponse.IsCommandSuccess, "File load should succeed");
|
||||
|
||||
// Verify we have active state
|
||||
var beforeStats = HotReloadRegistry.GetStats();
|
||||
Assert.Greater(beforeStats.ActiveOverrideCount, 0, "Should have active overrides before cleanup");
|
||||
|
||||
// Execute cleanup command
|
||||
PipelineResponse cleanupResponse = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("cleanup_hotreload", new
|
||||
{
|
||||
token = validToken,
|
||||
assemblyDir = "Temp/HotReload", // Use the default temp directory for testing
|
||||
force_domain_reload = false
|
||||
}, r => cleanupResponse = r);
|
||||
|
||||
Assert.IsTrue(cleanupResponse.IsCommandSuccess, $"Cleanup should succeed. Error: {cleanupResponse.CommandError}");
|
||||
|
||||
// Get message from nested result object
|
||||
var cleanupResult = cleanupResponse.JsonResponse["result"];
|
||||
var cleanupMessage = cleanupResult?["message"]?.ToString() ?? "";
|
||||
Assert.IsNotEmpty(cleanupMessage, "Should have cleanup message");
|
||||
Assert.That(cleanupMessage, Does.Contain("successful"), "Should indicate successful cleanup");
|
||||
|
||||
// Verify registry is cleared
|
||||
var afterStats = HotReloadRegistry.GetStats();
|
||||
Assert.AreEqual(0, afterStats.ActiveOverrideCount, "Should have 0 active overrides after cleanup");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator CLI_ReloadFile_WithInvalidFile_ReturnsError()
|
||||
{
|
||||
// Start server
|
||||
pipelineManager.StartServer();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
pipelineClient = new PipelineClient($"http://localhost:{pipelineManager.ActualPort}", validToken);
|
||||
|
||||
// Try to load non-existent file
|
||||
PipelineResponse response = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("reload_file_override", new
|
||||
{
|
||||
filename = "NonExistentFile.cs",
|
||||
token = validToken
|
||||
}, r => response = r);
|
||||
|
||||
// Should return error - check the nested result object
|
||||
Assert.IsNotNull(response, "Should get response");
|
||||
Assert.IsTrue(response.IsSuccess, "HTTP request should succeed");
|
||||
|
||||
// Get the actual command result from nested object
|
||||
var resultObj = response.JsonResponse["result"];
|
||||
var commandSuccess = resultObj?["success"]?.ToObject<bool>() ?? true;
|
||||
var commandError = resultObj?["error"]?.ToString() ?? "";
|
||||
|
||||
Assert.IsFalse(commandSuccess, "Command should fail for non-existent file");
|
||||
Assert.AreEqual("File Not Found", commandError, "Should return file not found error");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator CLI_ReloadFile_WithInvalidToken_ReturnsUnauthorized()
|
||||
{
|
||||
// Start server
|
||||
pipelineManager.StartServer();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
pipelineClient = new PipelineClient($"http://localhost:{pipelineManager.ActualPort}", "invalid-token");
|
||||
|
||||
// Try to reload with invalid token
|
||||
PipelineResponse response = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("reload_file_override", new
|
||||
{
|
||||
filename = "anything.cs",
|
||||
token = "invalid-token"
|
||||
}, r => response = r);
|
||||
|
||||
// Should return unauthorized - check the nested result object
|
||||
Assert.IsNotNull(response, "Should get response");
|
||||
Assert.IsTrue(response.IsSuccess, "HTTP request should succeed");
|
||||
|
||||
// Get the actual command result from nested object
|
||||
var resultObj = response.JsonResponse["result"];
|
||||
var commandSuccess = resultObj?["success"]?.ToObject<bool>() ?? true;
|
||||
var commandError = resultObj?["error"]?.ToString() ?? "";
|
||||
|
||||
Assert.IsFalse(commandSuccess, "Command should fail with invalid token");
|
||||
Assert.AreEqual("Unauthorized", commandError, "Should return unauthorized error");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator CLI_ReloadFile_WithSyntaxError_ReturnsCompilationError()
|
||||
{
|
||||
// Create file with syntax error
|
||||
var testFile = Path.Combine(testHotReloadDir, "SyntaxErrorTest.cs");
|
||||
File.WriteAllText(testFile, @"
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class SyntaxErrorTest
|
||||
{
|
||||
[HotReloadWithOverrides(Id = ""SyntaxError.Method"")]
|
||||
public void TestMethod() { }
|
||||
}
|
||||
|
||||
public static class SyntaxErrorHotReload
|
||||
{
|
||||
[HotReloadOverrideMethod(""SyntaxError.Method"")]
|
||||
public static void TestMethod(SyntaxErrorTest instance)
|
||||
{
|
||||
// Syntax error here
|
||||
var x = 2 +;
|
||||
}
|
||||
}");
|
||||
|
||||
// Start server
|
||||
pipelineManager.StartServer();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
pipelineClient = new PipelineClient($"http://localhost:{pipelineManager.ActualPort}", validToken);
|
||||
|
||||
// Try to reload file with syntax error
|
||||
PipelineResponse response = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("reload_file_override", new
|
||||
{
|
||||
filename = "CliTests/SyntaxErrorTest.cs",
|
||||
token = validToken
|
||||
}, r => response = r);
|
||||
|
||||
// Should return compilation error - check the nested result object
|
||||
Assert.IsNotNull(response, "Should get response");
|
||||
Assert.IsTrue(response.IsSuccess, "HTTP request should succeed");
|
||||
|
||||
// Get the actual command result from nested object
|
||||
var resultObj = response.JsonResponse["result"];
|
||||
var commandSuccess = resultObj?["success"]?.ToObject<bool>() ?? true;
|
||||
var commandError = resultObj?["error"]?.ToString() ?? "";
|
||||
|
||||
Assert.IsFalse(commandSuccess, "Command should fail with syntax error");
|
||||
Assert.AreEqual("Compilation Failed", commandError, "Should return compilation failed error");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator CLI_MultipleReloadFile_VersionTracking_Works()
|
||||
{
|
||||
// Test that multiple reloads of same file create versioned assemblies
|
||||
|
||||
var testFile = Path.Combine(testHotReloadDir, "VersionTest.cs");
|
||||
var baseContentTemplate = @"
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class VersionTestClass
|
||||
{
|
||||
[HotReloadWithOverrides(Id = ""VersionTest.Method"")]
|
||||
public void TestMethod() { }
|
||||
}
|
||||
|
||||
public static class VersionTestHotReload
|
||||
{
|
||||
[HotReloadOverrideMethod(""VersionTest.Method"")]
|
||||
public static void TestMethod(VersionTestClass instance)
|
||||
{
|
||||
UnityEngine.Debug.Log(""Version VERSION_NUMBER"");
|
||||
}
|
||||
}";
|
||||
|
||||
// Start server
|
||||
pipelineManager.StartServer();
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
|
||||
pipelineClient = new PipelineClient($"http://localhost:{pipelineManager.ActualPort}", validToken);
|
||||
|
||||
// Load same file multiple times
|
||||
for (int i = 1; i <= 3; i++)
|
||||
{
|
||||
// Expected error: signature mismatch for versions 2+ due to cross-assembly type incompatibility
|
||||
if (i > 1)
|
||||
{
|
||||
LogAssert.Expect(LogType.Error, "HotReload: Signature mismatch for 'VersionTest.Method'. Override method must have instance parameter as first argument.");
|
||||
}
|
||||
|
||||
// Update file content with version number using string replacement
|
||||
var content = baseContentTemplate.Replace("VERSION_NUMBER", i.ToString());
|
||||
File.WriteAllText(testFile, content);
|
||||
|
||||
PipelineResponse response = null;
|
||||
yield return pipelineClient.ExecuteCommandAsync("reload_file_override", new
|
||||
{
|
||||
filename = "CliTests/VersionTest.cs",
|
||||
token = validToken
|
||||
}, r => response = r);
|
||||
|
||||
Assert.IsTrue(response.IsCommandSuccess, $"Version {i} reload should succeed");
|
||||
|
||||
// Extract assembly name from nested result object
|
||||
if (response.HasValidJson)
|
||||
{
|
||||
var resultObj = response.JsonResponse["result"];
|
||||
var assemblyName = resultObj?["AssemblyName"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(assemblyName))
|
||||
{
|
||||
Assert.That(assemblyName, Does.Contain($"_{i:D3}"), $"Version {i} should have correct version suffix");
|
||||
}
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.1f); // Small delay between versions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user