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 { /// /// Integration tests for hot reload CLI commands through the Pipeline Server. /// Tests the complete workflow: CLI commands → Pipeline Server → Hot Reload Compilation → Registry. /// [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(); // 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() ?? 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() ?? 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() ?? 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 } } } }