using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Unity.Pipeline.Security; using Unity.Pipeline.Tests.Runtime; namespace Unity.Pipeline.Tests.Editor { /// /// Test fixture that owns an isolated plus a /// pointed at it. ViaClient tests use this instead of the live /// editor server, so running the suite never disturbs the server agents drive over HTTP. /// /// Wrap in a `using` (or SetUp/TearDown) so the server is stopped after each test. /// sealed class PipelineTestServer : IDisposable { private readonly TestEditorPipelineServer m_Server; private readonly PipelineClient m_Client; // Appended from the HTTP thread, read by the test afterwards. private readonly ConcurrentQueue m_Reported = new ConcurrentQueue(); private readonly ConcurrentQueue m_ReportedOnMainThread = new ConcurrentQueue(); public int Port => m_Server.Port; public PipelineClient Client => m_Client; /// /// Everything the server reported when a command finished, oldest first — including a /// request rejected before any command ran, which carries a transaction and no command. /// public IReadOnlyList Reported => m_Reported.ToArray(); /// /// What crossed to the main thread, which is the subset carrying a command. The crossing is /// a dispatcher post, normally pumped by the editor update loop; that loop does not tick /// inside a synchronous test, so pumps it before returning. /// public IReadOnlyList ReportedOnMainThread => m_ReportedOnMainThread.ToArray(); public PipelineTestServer() { m_Server = new TestEditorPipelineServer(); m_Server.CommandDoneRecorder = info => m_Reported.Enqueue(info); m_Server.CommandDoneMainThreadRecorder = info => m_ReportedOnMainThread.Enqueue(info); m_Server.Start(); // auto-assigns a port in 7850-7899; writes no descriptor m_Client = new PipelineClient($"http://localhost:{m_Server.Port}", SecurityTokenManager.GetOrCreateToken()); } /// /// Execute a command against this isolated server, pumping the server's own dispatcher /// while the HTTP call is in flight. This lets MainThreadRequired commands complete even /// when the editor update loop isn't ticking (e.g. during a synchronous run_tests that /// blocks the main thread), so it's safe to call from a plain [Test] without deadlocking. /// public PipelineResponse Execute(string command, object parameters = null, int timeoutMs = 30000) { // Run the whole client call on a threadpool thread (not the main thread) so none of its // async continuations capture Unity's SynchronizationContext. Otherwise they'd be queued // back to the main thread, which this method deliberately blocks below while pumping — // the task could never complete (sync-over-async deadlock). The server side still needs // the main thread, which the ProcessWorkQueue pump below provides. var reportedBefore = m_Reported.Count; var task = Task.Run(() => m_Client.ExecuteCommandAsync(command, parameters)); var start = DateTime.UtcNow; while (!task.IsCompleted) { m_Server.Dispatcher.ProcessWorkQueue(); if ((DateTime.UtcNow - start).TotalMilliseconds > timeoutMs) throw new TimeoutException($"Command '{command}' did not complete within {timeoutMs}ms"); Thread.Sleep(1); } // The server writes the response bytes first and reports the finished interaction // afterwards, still on its HTTP thread, so the client task can complete a few // milliseconds before OnCommandDone runs. Wait for that report (bounded), otherwise a // test reading Reported right after Execute races it. Then pump once more, standing in // for the editor update tick, so ReportedOnMainThread is current on return too. var reportWait = DateTime.UtcNow; while (m_Reported.Count <= reportedBefore && (DateTime.UtcNow - reportWait).TotalMilliseconds < 2000) { m_Server.Dispatcher.ProcessWorkQueue(); Thread.Sleep(1); } m_Server.Dispatcher.ProcessWorkQueue(int.MaxValue); return task.GetAwaiter().GetResult(); } public void Dispose() { m_Client?.Dispose(); m_Server?.Stop(); } } }