Add Unity Pipeline package support
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// CLI-224 fixture: an already-compiled MonoBehaviour used to exercise attach_script's --script
|
||||
/// (asset path) addressing — the command resolves the backing class from the .cs asset via
|
||||
/// MonoScript.GetClass(), so an agent can pass the create_script assetPath directly.
|
||||
///
|
||||
/// It deliberately lives in the RUNTIME test assembly (Unity.Pipeline.Tests.Runtime, all-platforms),
|
||||
/// NOT the editor test assembly: Unity refuses AddComponent for a MonoBehaviour whose dedicated
|
||||
/// MonoScript belongs to an editor-only assembly, so a real attach must target a runtime script —
|
||||
/// which also matches how agents author scripts under Assets/. The class name matches the file name
|
||||
/// (Unity requires that for a single-type script to be addable).
|
||||
/// </summary>
|
||||
public class AttachByPathFixture : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int m_Value;
|
||||
|
||||
public int Value => m_Value;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ba2fadc593de9d4994b993941573feb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d346a8659db07c4d9ac7ba1568ed981
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Unity.Pipeline.HotReload;
|
||||
using Unity.Pipeline.Runtime.Commands;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration tests for in-place hot reload editing workflow.
|
||||
/// Tests the complete pipeline: source parsing -> validation -> transformation -> compilation.
|
||||
/// </summary>
|
||||
[Ignore("HotReload in-place editing is deferred until the autonomous test loop is solid; this path exercises the known instance-to-static transformation bug. Re-enable when revisiting in-place reload.")]
|
||||
public class HotReloadInPlaceEditingTests
|
||||
{
|
||||
private string _testFilePath;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Clean registry before each test
|
||||
HotReloadRegistry.ClearAllForTesting();
|
||||
|
||||
// Create temp test file path
|
||||
_testFilePath = Path.Combine(Application.temporaryCachePath, "HotReloadExampleComponent.cs");
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Clean up test file
|
||||
if (File.Exists(_testFilePath))
|
||||
{
|
||||
File.Delete(_testFilePath);
|
||||
}
|
||||
|
||||
// Clean registry after test
|
||||
HotReloadRegistry.ClearAllForTesting();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator InPlaceReload_ValidPublicMemberAccess_ShouldSucceed()
|
||||
{
|
||||
// Arrange: Create source file with [HotReloadWithOverrides] method using only public members
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class HotReloadExampleComponent : MonoBehaviour
|
||||
{
|
||||
public float speed = 5.0f;
|
||||
public bool isActive = true;
|
||||
|
||||
[HotReload]
|
||||
void Update()
|
||||
{
|
||||
if (isActive)
|
||||
{
|
||||
transform.position += Vector3.right * speed * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(_testFilePath, sourceCode);
|
||||
|
||||
// Act: Execute in-place reload command
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
return await InPlaceReloadProcessor.ProcessSourceFileAsync(_testFilePath);
|
||||
});
|
||||
|
||||
yield return new WaitUntil(() => task.IsCompleted);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(task.Result.Success, $"In-place reload should succeed. Error: {task.Result.ErrorMessage}");
|
||||
Assert.AreEqual("HotReloadExampleComponent", task.Result.OriginalTypeName);
|
||||
Assert.Contains("Update", task.Result.ExtractedMethods);
|
||||
Assert.IsTrue(task.Result.RegisteredMethods.Count > 0, "Should have registered at least one method");
|
||||
Assert.IsNotNull(task.Result.TransformedCode, "Should have generated transformed code");
|
||||
|
||||
// Verify registry state
|
||||
var stats = HotReloadRegistry.GetStats();
|
||||
Assert.IsTrue(stats.ActiveOverrideCount > 0, "Should have active overrides registered");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator InPlaceReload_PrivateMemberAccess_ShouldFail()
|
||||
{
|
||||
// Arrange: Create source file with [HotReloadWithOverrides] method trying to access private members
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class HotReloadExampleComponent : MonoBehaviour
|
||||
{
|
||||
private float privateSpeed = 5.0f;
|
||||
public bool isActive = true;
|
||||
|
||||
[HotReload]
|
||||
void Update()
|
||||
{
|
||||
if (isActive)
|
||||
{
|
||||
transform.position += Vector3.right * privateSpeed * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(_testFilePath, sourceCode);
|
||||
|
||||
// Act: Execute in-place reload command
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
return await InPlaceReloadProcessor.ProcessSourceFileAsync(_testFilePath);
|
||||
});
|
||||
|
||||
yield return new WaitUntil(() => task.IsCompleted);
|
||||
|
||||
// Assert: Should fail due to private member access
|
||||
Assert.IsFalse(task.Result.Success, "In-place reload should fail for private member access");
|
||||
Assert.IsNotNull(task.Result.ErrorMessage, "Should have error message");
|
||||
Assert.IsTrue(task.Result.ErrorMessage.Contains("privateSpeed") ||
|
||||
task.Result.ErrorMessage.Contains("private"),
|
||||
$"Error message should mention private member. Actual: {task.Result.ErrorMessage}");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator InPlaceReload_MultipleHotReloadableMethods_ShouldSucceed()
|
||||
{
|
||||
// Arrange: Create source file with multiple [HotReloadWithOverrides] methods
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.HotReload;
|
||||
using Unity.Pipeline.Samples.HotReload;
|
||||
|
||||
public class HotReloadExampleComponent : MonoBehaviour
|
||||
{
|
||||
public float speed = 5.0f;
|
||||
public int health = 100;
|
||||
|
||||
[HotReload]
|
||||
void Update()
|
||||
{
|
||||
transform.position += Vector3.right * speed * Time.deltaTime;
|
||||
}
|
||||
|
||||
[HotReload]
|
||||
public int CalculateDamage(int baseDamage)
|
||||
{
|
||||
return baseDamage * 2;
|
||||
}
|
||||
|
||||
[HotReload]
|
||||
public void ResetPosition()
|
||||
{
|
||||
transform.position = Vector3.zero;
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(_testFilePath, sourceCode);
|
||||
|
||||
// Act: Execute in-place reload command
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
return await InPlaceReloadProcessor.ProcessSourceFileAsync(_testFilePath);
|
||||
});
|
||||
|
||||
yield return new WaitUntil(() => task.IsCompleted);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(task.Result.Success, $"In-place reload should succeed. Error: {task.Result.ErrorMessage}");
|
||||
Assert.AreEqual(3, task.Result.ExtractedMethods.Count, "Should extract 3 [HotReloadWithOverrides] methods");
|
||||
Assert.Contains("Update", task.Result.ExtractedMethods);
|
||||
Assert.Contains("CalculateDamage", task.Result.ExtractedMethods);
|
||||
Assert.Contains("ResetPosition", task.Result.ExtractedMethods);
|
||||
|
||||
// Verify transformed code contains all methods
|
||||
Assert.IsTrue(task.Result.TransformedCode.Contains("[HotReloadOverrideMethod(\"HotReloadExampleComponent.Update\")]"));
|
||||
Assert.IsTrue(task.Result.TransformedCode.Contains("[HotReloadOverrideMethod(\"HotReloadExampleComponent.CalculateDamage\")]"));
|
||||
Assert.IsTrue(task.Result.TransformedCode.Contains("[HotReloadOverrideMethod(\"HotReloadExampleComponent.ResetPosition\")]"));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ReloadFileCommand_InPlaceSourceFile_ShouldUseInPlaceWorkflow()
|
||||
{
|
||||
// Arrange: Create source file with [HotReloadWithOverrides] method
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class HotReloadExampleComponent : MonoBehaviour
|
||||
{
|
||||
public float speed = 5.0f;
|
||||
|
||||
[HotReload]
|
||||
void Update()
|
||||
{
|
||||
transform.position += Vector3.up * speed * Time.deltaTime;
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(_testFilePath, sourceCode);
|
||||
|
||||
// Act: Execute reload_file command
|
||||
var response = HotReloadCommands.ReloadFile(_testFilePath);
|
||||
|
||||
// Assert
|
||||
yield return null; // Allow frame to complete
|
||||
|
||||
Assert.IsTrue(response.Success, $"reload_file command should succeed. Error: {response.ErrorDetails}");
|
||||
Assert.IsNotNull(response.AssemblyName, "Should have assembly name");
|
||||
Assert.IsTrue(response.Items.Count > 0, "Should have registered methods");
|
||||
|
||||
// Verify registry state
|
||||
var stats = HotReloadRegistry.GetStats();
|
||||
Assert.IsTrue(stats.ActiveOverrideCount > 0, "Should have active overrides registered");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ReloadFileCommand_SeparateOverrideFile_ShouldUseSeparateWorkflow()
|
||||
{
|
||||
// Arrange: Create separate override file in HotReload/ directory
|
||||
var hotReloadDir = Path.Combine(Application.temporaryCachePath, "HotReload");
|
||||
Directory.CreateDirectory(hotReloadDir);
|
||||
var overrideFilePath = Path.Combine(hotReloadDir, "TestOverrides.cs");
|
||||
|
||||
var overrideCode = @"
|
||||
using Unity.Pipeline.HotReload;
|
||||
using UnityEngine;
|
||||
|
||||
public static class TestOverrides
|
||||
{
|
||||
[HotReloadOverrideMethod(""TestComponent.Update"")]
|
||||
public static void Update(MonoBehaviour instance)
|
||||
{
|
||||
instance.transform.position += Vector3.down * Time.deltaTime;
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(overrideFilePath, overrideCode);
|
||||
|
||||
try
|
||||
{
|
||||
// Act: Execute reload_file_override command with HotReload/ path
|
||||
var response = HotReloadCommands.ReloadFileOverride("HotReload/TestOverrides.cs");
|
||||
|
||||
// Assert
|
||||
yield return null; // Allow frame to complete
|
||||
|
||||
// This might fail due to missing target method, but should use separate workflow
|
||||
// The important thing is that it recognizes it as separate override file
|
||||
Assert.IsNotNull(response, "Should get a response");
|
||||
// Note: This test verifies workflow detection rather than successful compilation
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
if (File.Exists(overrideFilePath))
|
||||
{
|
||||
File.Delete(overrideFilePath);
|
||||
}
|
||||
if (Directory.Exists(hotReloadDir))
|
||||
{
|
||||
Directory.Delete(hotReloadDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator AccessibilityValidation_MixedAccess_ShouldReportSpecificViolations()
|
||||
{
|
||||
// Arrange: Create source with both valid and invalid member access
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class HotReloadExampleComponent : MonoBehaviour
|
||||
{
|
||||
public float publicSpeed = 5.0f;
|
||||
private float privateSpeed = 3.0f;
|
||||
internal int internalHealth = 100;
|
||||
|
||||
[HotReload]
|
||||
void Update()
|
||||
{
|
||||
// Valid public access
|
||||
transform.position += Vector3.right * publicSpeed * Time.deltaTime;
|
||||
|
||||
// Invalid private access
|
||||
transform.position += Vector3.up * privateSpeed * Time.deltaTime;
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(_testFilePath, sourceCode);
|
||||
|
||||
// Act: Execute in-place reload command
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
return await InPlaceReloadProcessor.ProcessSourceFileAsync(_testFilePath);
|
||||
});
|
||||
|
||||
yield return new WaitUntil(() => task.IsCompleted);
|
||||
|
||||
// Assert: Should fail with specific violation details
|
||||
Assert.IsFalse(task.Result.Success, "Should fail due to private member access");
|
||||
Assert.IsTrue(task.Result.ValidationViolations.Count > 0, "Should have validation violations");
|
||||
|
||||
var violation = task.Result.ValidationViolations[0];
|
||||
Assert.IsTrue(violation.MemberName.Contains("privateSpeed"), $"Should identify privateSpeed as violation. Got: {violation.MemberName}");
|
||||
Assert.IsNotNull(violation.Suggestion, "Should provide suggestion for fix");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator TransformedCode_ThisReferences_ShouldBeConvertedToInstance()
|
||||
{
|
||||
// Arrange: Create source with 'this.' references
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class HotReloadExampleComponent : MonoBehaviour
|
||||
{
|
||||
public float speed = 5.0f;
|
||||
|
||||
[HotReload]
|
||||
void Update()
|
||||
{
|
||||
this.transform.position += Vector3.right * this.speed * Time.deltaTime;
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(_testFilePath, sourceCode);
|
||||
|
||||
// Act: Execute in-place reload command
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
return await InPlaceReloadProcessor.ProcessSourceFileAsync(_testFilePath);
|
||||
});
|
||||
|
||||
yield return new WaitUntil(() => task.IsCompleted);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(task.Result.Success, $"Should succeed. Error: {task.Result.ErrorMessage}");
|
||||
|
||||
var transformedCode = task.Result.TransformedCode;
|
||||
Assert.IsNotNull(transformedCode, "Should have transformed code");
|
||||
|
||||
// Verify 'this.' is converted to 'instance.'
|
||||
Assert.IsFalse(transformedCode.Contains("this."), "Transformed code should not contain 'this.' references");
|
||||
Assert.IsTrue(transformedCode.Contains("instance."), "Transformed code should contain 'instance.' references");
|
||||
Assert.IsTrue(transformedCode.Contains("instance.transform"), "Should convert this.transform to instance.transform");
|
||||
Assert.IsTrue(transformedCode.Contains("instance.speed"), "Should convert this.speed to instance.speed");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator MethodSignatures_WithParameters_ShouldPreserveParameterInfo()
|
||||
{
|
||||
// Arrange: Create source with parameterized [HotReloadWithOverrides] method
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class HotReloadExampleComponent : MonoBehaviour
|
||||
{
|
||||
public float speed = 5.0f;
|
||||
|
||||
[HotReload]
|
||||
public void MoveTowards(Vector3 target, float customSpeed)
|
||||
{
|
||||
var direction = (target - transform.position).normalized;
|
||||
transform.position += direction * customSpeed * Time.deltaTime;
|
||||
}
|
||||
}";
|
||||
|
||||
File.WriteAllText(_testFilePath, sourceCode);
|
||||
|
||||
// Act: Execute in-place reload command
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
return await InPlaceReloadProcessor.ProcessSourceFileAsync(_testFilePath);
|
||||
});
|
||||
|
||||
yield return new WaitUntil(() => task.IsCompleted);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(task.Result.Success, $"Should succeed. Error: {task.Result.ErrorMessage}");
|
||||
|
||||
var transformedCode = task.Result.TransformedCode;
|
||||
Assert.IsNotNull(transformedCode, "Should have transformed code");
|
||||
|
||||
// Verify method signature includes instance parameter plus original parameters
|
||||
Assert.IsTrue(transformedCode.Contains("MoveTowards(HotReloadExampleComponent instance, Vector3 target, float customSpeed)"),
|
||||
$"Should preserve method parameters with instance parameter first. Actual:\n{transformedCode}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContainsHotReloadableMethods_ValidFile_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
using Unity.Pipeline.HotReload;
|
||||
|
||||
public class TestComponent : MonoBehaviour
|
||||
{
|
||||
[HotReload]
|
||||
void Update() { }
|
||||
}";
|
||||
|
||||
var testPath = Path.Combine(Application.temporaryCachePath, "TestCheck.cs");
|
||||
File.WriteAllText(testPath, sourceCode);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
var result = InPlaceReloadProcessor.ContainsHotReloadableMethodsAsync(testPath).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result, "Should detect [HotReloadWithOverrides] methods");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(testPath))
|
||||
{
|
||||
File.Delete(testPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContainsHotReloadableMethods_NoHotReloadMethods_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var sourceCode = @"
|
||||
using UnityEngine;
|
||||
|
||||
public class TestComponent : MonoBehaviour
|
||||
{
|
||||
void Update() { }
|
||||
}";
|
||||
|
||||
var testPath = Path.Combine(Application.temporaryCachePath, "TestCheck.cs");
|
||||
File.WriteAllText(testPath, sourceCode);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
var result = InPlaceReloadProcessor.ContainsHotReloadableMethodsAsync(testPath).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Should not detect [HotReloadWithOverrides] methods");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(testPath))
|
||||
{
|
||||
File.Delete(testPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2789a86fdd0d2cc4ebea1b80a5b428d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,412 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Unified client for making HTTP requests to Pipeline server in tests.
|
||||
/// Provides consistent JSON payload structure and response parsing.
|
||||
/// </summary>
|
||||
public class PipelineClient : IDisposable
|
||||
{
|
||||
private readonly string m_BaseUrl;
|
||||
private readonly string m_AuthToken;
|
||||
private HttpClient m_HttpClient;
|
||||
private bool m_Disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Create a Pipeline client for the specified server.
|
||||
/// </summary>
|
||||
/// <param name="baseUrl">Base URL of the Pipeline server (e.g., "http://localhost:7900")</param>
|
||||
/// <param name="authToken">Optional authentication token</param>
|
||||
public PipelineClient(string baseUrl, string authToken = null)
|
||||
{
|
||||
m_BaseUrl = baseUrl.TrimEnd('/');
|
||||
m_AuthToken = authToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a client for a running server, deriving the URL (port) and auth token from it.
|
||||
/// The server must be started so its token is available.
|
||||
/// </summary>
|
||||
public PipelineClient(BasePipelineServer server)
|
||||
: this($"http://localhost:{server.Port}", server.Token)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a client for a runtime server owned by a <see cref="RuntimePipelineManager"/>.
|
||||
/// The manager must have started its server.
|
||||
/// </summary>
|
||||
public PipelineClient(RuntimePipelineManager manager)
|
||||
: this(manager.Server)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize HttpClient for async operations (lazy initialization).
|
||||
/// </summary>
|
||||
private HttpClient GetHttpClient()
|
||||
{
|
||||
if (m_HttpClient == null)
|
||||
{
|
||||
m_HttpClient = new HttpClient();
|
||||
if (!string.IsNullOrEmpty(m_AuthToken))
|
||||
{
|
||||
m_HttpClient.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", m_AuthToken);
|
||||
}
|
||||
}
|
||||
return m_HttpClient;
|
||||
}
|
||||
|
||||
// Coroutine forms for [UnityTest]. They drive the async request by polling the Task each
|
||||
// frame (yield return null) rather than yielding a UnityWebRequest AsyncOperation: the
|
||||
// EditMode test runner does not await an AsyncOperation yielded from a nested enumerator,
|
||||
// and frame-pumping also keeps the editor/player update loop (and the server's main-thread
|
||||
// dispatcher) ticking so main-thread commands can complete.
|
||||
|
||||
/// <summary>GET an endpoint (coroutine form for [UnityTest]).</summary>
|
||||
public IEnumerator GetAsync(string endpoint, System.Action<PipelineResponse> onComplete)
|
||||
=> Await(GetAsync(endpoint), onComplete);
|
||||
|
||||
/// <summary>Get server status via /api/status endpoint (coroutine form).</summary>
|
||||
public IEnumerator GetStatusAsync(System.Action<PipelineResponse> onComplete)
|
||||
=> GetAsync("/api/status", onComplete);
|
||||
|
||||
/// <summary>Execute a command via /api/exec endpoint (coroutine form).</summary>
|
||||
public IEnumerator ExecuteCommandAsync(string command, object parameters, System.Action<PipelineResponse> onComplete)
|
||||
=> Await(ExecuteCommandAsync(command, parameters), onComplete);
|
||||
|
||||
/// <summary>Execute a code evaluation command (coroutine form).</summary>
|
||||
public IEnumerator ExecuteCodeAsync(string code, int timeout, System.Action<PipelineResponse> onComplete)
|
||||
=> Await(ExecuteCodeAsync(code, timeout), onComplete);
|
||||
|
||||
/// <summary>Post JSON to an endpoint (coroutine form).</summary>
|
||||
public IEnumerator PostJsonAsync(string endpoint, object payload, System.Action<PipelineResponse> onComplete)
|
||||
=> Await(PostJsonAsync(endpoint, payload), onComplete);
|
||||
|
||||
private static IEnumerator Await(Task<PipelineResponse> task, System.Action<PipelineResponse> onComplete)
|
||||
{
|
||||
while (!task.IsCompleted)
|
||||
yield return null;
|
||||
onComplete?.Invoke(task.Result);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ASYNC METHODS (for Editor tests using async/await)
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Get server status via /api/status endpoint (async version).
|
||||
/// </summary>
|
||||
public Task<PipelineResponse> GetStatusAsync() => GetAsync("/api/status");
|
||||
|
||||
/// <summary>
|
||||
/// GET an endpoint (async version), returning a parsed <see cref="PipelineResponse"/>.
|
||||
/// </summary>
|
||||
public async Task<PipelineResponse> GetAsync(string endpoint)
|
||||
{
|
||||
var url = $"{m_BaseUrl}{endpoint}";
|
||||
|
||||
try
|
||||
{
|
||||
var httpClient = GetHttpClient();
|
||||
var httpResponse = await httpClient.GetAsync(url);
|
||||
var content = await httpResponse.Content.ReadAsStringAsync();
|
||||
|
||||
return CreateResponseFromHttp(httpResponse, content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PipelineResponse
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = ex.Message,
|
||||
RawResponse = ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a command via /api/exec endpoint (async version).
|
||||
/// </summary>
|
||||
public async Task<PipelineResponse> ExecuteCommandAsync(string command, object parameters)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
command = command,
|
||||
parameters = parameters ?? new { }
|
||||
};
|
||||
|
||||
return await PostJsonAsync("/api/exec", payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a code evaluation command (async version).
|
||||
/// </summary>
|
||||
public async Task<PipelineResponse> ExecuteCodeAsync(string code, int timeout)
|
||||
{
|
||||
var parameters = new
|
||||
{
|
||||
code = code,
|
||||
timeout = timeout
|
||||
};
|
||||
|
||||
return await ExecuteCommandAsync("eval", parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Post JSON data to an endpoint (async version).
|
||||
/// </summary>
|
||||
public async Task<PipelineResponse> PostJsonAsync(string endpoint, object payload)
|
||||
{
|
||||
var url = $"{m_BaseUrl}{endpoint}";
|
||||
var jsonData = JsonConvert.SerializeObject(payload, Formatting.Indented);
|
||||
var content = new StringContent(jsonData, Encoding.UTF8, "application/json");
|
||||
|
||||
try
|
||||
{
|
||||
var httpClient = GetHttpClient();
|
||||
var httpResponse = await httpClient.PostAsync(url, content);
|
||||
var responseContent = await httpResponse.Content.ReadAsStringAsync();
|
||||
|
||||
return CreateResponseFromHttp(httpResponse, responseContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PipelineResponse
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = ex.Message,
|
||||
RawResponse = ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get raw HTTP response (async version).
|
||||
/// </summary>
|
||||
public async Task<HttpResponseMessage> GetHttpAsync(string endpoint)
|
||||
{
|
||||
var url = $"{m_BaseUrl}{endpoint}";
|
||||
var httpClient = GetHttpClient();
|
||||
return await httpClient.GetAsync(url);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SHARED HELPER METHODS
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Create a PipelineResponse from HttpResponseMessage.
|
||||
/// </summary>
|
||||
private PipelineResponse CreateResponseFromHttp(HttpResponseMessage httpResponse, string content)
|
||||
{
|
||||
var response = new PipelineResponse
|
||||
{
|
||||
IsSuccess = httpResponse.IsSuccessStatusCode,
|
||||
StatusCode = (int)httpResponse.StatusCode,
|
||||
Error = httpResponse.IsSuccessStatusCode ? null : httpResponse.ReasonPhrase,
|
||||
RawResponse = content ?? "",
|
||||
HttpResponse = httpResponse
|
||||
};
|
||||
|
||||
// Try to parse JSON response
|
||||
if (!string.IsNullOrEmpty(response.RawResponse))
|
||||
{
|
||||
try
|
||||
{
|
||||
response.JsonResponse = JObject.Parse(response.RawResponse);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
response.ParseError = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of any resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!m_Disposed)
|
||||
{
|
||||
m_HttpClient?.Dispose();
|
||||
m_Disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standardized response from Pipeline server.
|
||||
/// </summary>
|
||||
public class PipelineResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the HTTP request succeeded.
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP status code.
|
||||
/// </summary>
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP error message (if any).
|
||||
/// </summary>
|
||||
public string Error { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw response text.
|
||||
/// </summary>
|
||||
public string RawResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parsed JSON response (null if parsing failed).
|
||||
/// </summary>
|
||||
public JObject JsonResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JSON parsing error (if any).
|
||||
/// </summary>
|
||||
public string ParseError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original HttpResponseMessage (for advanced scenarios with async methods).
|
||||
/// </summary>
|
||||
public HttpResponseMessage HttpResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the response has valid JSON.
|
||||
/// </summary>
|
||||
public bool HasValidJson => JsonResponse != null && string.IsNullOrEmpty(ParseError);
|
||||
|
||||
/// <summary>
|
||||
/// Get a typed response object from the JSON.
|
||||
/// </summary>
|
||||
public T GetTypedResponse<T>() where T : class
|
||||
{
|
||||
if (!HasValidJson || JsonResponse == null || !JsonResponse.ContainsKey("result"))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonResponse["result"].ToObject<T>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a JSON property value.
|
||||
/// </summary>
|
||||
public T GetProperty<T>(string propertyName, T defaultValue = default(T))
|
||||
{
|
||||
if (!HasValidJson)
|
||||
return defaultValue;
|
||||
|
||||
var token = JsonResponse[propertyName];
|
||||
if (token == null)
|
||||
return defaultValue;
|
||||
|
||||
try
|
||||
{
|
||||
return token.ToObject<T>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the command execution was successful (different from HTTP success).
|
||||
/// </summary>
|
||||
public bool IsCommandSuccess => GetProperty("success", false);
|
||||
|
||||
/// <summary>
|
||||
/// Get command error message.
|
||||
/// </summary>
|
||||
public string CommandError => GetProperty<string>("error");
|
||||
|
||||
/// <summary>
|
||||
/// Get command error details.
|
||||
/// </summary>
|
||||
public string CommandErrorDetails => GetProperty<string>("errorDetails");
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var status = IsSuccess ? "Success" : "Failed";
|
||||
var content = HasValidJson ? "JSON" : "Raw";
|
||||
return $"{status} ({StatusCode}) - {content}: {RawResponse?.Substring(0, Math.Min(100, RawResponse.Length))}...";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for common Pipeline operations.
|
||||
/// </summary>
|
||||
public static class PipelineClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Execute code evaluation and get strongly-typed response (coroutine version).
|
||||
/// </summary>
|
||||
public static IEnumerator ExecuteCodeWithResponseAsync(this PipelineClient client, string code, int timeout, System.Action<Unity.Pipeline.Models.EvalResponse> onComplete)
|
||||
{
|
||||
yield return client.ExecuteCodeAsync(code, timeout, response =>
|
||||
{
|
||||
if (response.HasValidJson)
|
||||
{
|
||||
var evalResponse = response.GetTypedResponse<Unity.Pipeline.Models.EvalResponse>();
|
||||
onComplete?.Invoke(evalResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create error response
|
||||
var errorResponse = new Unity.Pipeline.Models.EvalResponse
|
||||
{
|
||||
Success = false,
|
||||
Error = response.Error ?? "HTTP Error",
|
||||
ErrorDetails = response.RawResponse ?? "No response data"
|
||||
};
|
||||
onComplete?.Invoke(errorResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute code evaluation and get strongly-typed response (async version).
|
||||
/// </summary>
|
||||
public static async Task<Unity.Pipeline.Models.EvalResponse> ExecuteCodeWithResponseAsync(this PipelineClient client, string code, int timeout = 5000)
|
||||
{
|
||||
var response = await client.ExecuteCodeAsync(code, timeout);
|
||||
|
||||
if (response.HasValidJson)
|
||||
{
|
||||
var evalResponse = response.GetTypedResponse<Unity.Pipeline.Models.EvalResponse>();
|
||||
if (evalResponse != null)
|
||||
return evalResponse;
|
||||
}
|
||||
|
||||
// Create error response if parsing failed
|
||||
return new Unity.Pipeline.Models.EvalResponse
|
||||
{
|
||||
Success = false,
|
||||
Error = response.Error ?? "HTTP Error",
|
||||
ErrorDetails = response.RawResponse ?? "No response data"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e682207faf781384a843402bf94e2f44
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
using System.Collections;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Unity.Pipeline.Runtime.Commands;
|
||||
using Unity.Pipeline.Security;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Unity.Pipeline.Tests.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin PlayMode smoke suite for paths that genuinely require a running player:
|
||||
/// the autoStart lifecycle (MonoBehaviour Start) and quit (DontDestroyOnLoad, play-mode only).
|
||||
/// General command behavior is covered by the EditMode *CommandTests — this only covers
|
||||
/// play-mode-only paths and one end-to-end server check.
|
||||
/// </summary>
|
||||
public class RuntimeServerPlayModeSmokeTests
|
||||
{
|
||||
[UnityTest]
|
||||
public IEnumerator AutoStart_StartsServer_AndServesRequest()
|
||||
{
|
||||
var go = new GameObject("SmokeRuntimeManager");
|
||||
var mgr = go.AddComponent<RuntimePipelineManager>();
|
||||
mgr.enableInBuilds = true;
|
||||
mgr.autoStart = true;
|
||||
|
||||
// Enabling the GameObject runs Start() -> autoStart -> StartServer.
|
||||
go.SetActive(true);
|
||||
|
||||
float t = 0f;
|
||||
while (!mgr.IsServerRunning && t < 5f) { t += Time.unscaledDeltaTime; yield return null; }
|
||||
Assert.IsTrue(mgr.IsServerRunning, "autoStart should start the runtime server");
|
||||
|
||||
var serve = ServeStatus(mgr.ActualPort, SecurityTokenManager.GetOrCreateToken());
|
||||
while (!serve.IsCompleted) yield return null;
|
||||
Assert.IsTrue(serve.Result, "runtime server should serve a runtime_status request");
|
||||
|
||||
mgr.StopServer();
|
||||
Object.Destroy(go);
|
||||
}
|
||||
|
||||
static async Task<bool> ServeStatus(int port, string token)
|
||||
{
|
||||
using (var client = new PipelineClient($"http://localhost:{port}", token))
|
||||
{
|
||||
var response = await client.ExecuteCommandAsync("runtime_status", null);
|
||||
return response.IsSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Quit_SchedulesQuit()
|
||||
{
|
||||
// QuitApplication uses DontDestroyOnLoad, which is play-mode only. Application.Quit is a
|
||||
// no-op in the editor, but destroy the scheduler anyway so nothing lingers.
|
||||
var result = RuntimeApplicationCommand.QuitApplication(0);
|
||||
Assert.That(result, Does.Contain("Application quit scheduled with exit code 0"));
|
||||
|
||||
foreach (var go in PipelineUtils.FindObjectsByType<GameObject>())
|
||||
{
|
||||
if (go.name.Contains("QuitScheduler")) { Object.Destroy(go); break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 062e8d6875208134e8cf794e735c719b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Unity.Pipeline.Tests.Runtime",
|
||||
"rootNamespace": "Unity.Pipeline.Tests.Runtime",
|
||||
"references": [
|
||||
"UnityEngine.TestRunner",
|
||||
"Unity.Pipeline"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [
|
||||
"nunit.framework.dll"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74fd6218aa624d8458bbde685e04b0c6
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user