Files

412 lines
14 KiB
C#

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"
};
}
}
}