using System.Windows; using System.Windows.Threading; namespace CursorLang.Settings.Tests.Infrastructure; /// /// The user interface thread for the tests. /// /// /// Half of the application — windows, the dispatcher and the timers on it — /// only works on an STA thread with a message queue, while tests run on a pool /// thread. The thread is therefore started once for the whole run: a process /// may hold only one , and recreating it between tests /// is not possible. /// /// The queue on that thread is pumped for real, so timers fire on their own: /// the test only has to await the consequences through . /// internal static class Sta { private static readonly Lock Gate = new(); private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(5); internal static Dispatcher Dispatcher { get { lock (Gate) { return field ??= Start(); } } } /// Runs an action on the interface thread and waits for it to finish. internal static void Run(Action action) => Dispatcher.Invoke(action); /// The same for an action that returns a result. internal static TResult Run(Func action) => Dispatcher.Invoke(action); /// /// Runs an action on a separate STA thread and waits for it to finish. /// /// /// Needed where the foreignness of the thread is the point: kernel objects /// such as a mutex let their own owner in again, so a "second instance" of /// the application on the same thread does not count as second. /// internal static void RunApart(Action action) { Exception? failure = null; var thread = new Thread(() => { try { action(); } catch (Exception e) { failure = e; } }) { IsBackground = true, Name = "CursorLang.Tests apart", }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); if (failure is not null) { throw new InvalidOperationException("The action on the separate thread failed", failure); } } /// /// Waits until the dispatcher queue drains: calls deferred through /// BeginInvoke have run by that time. /// internal static void Drain() => Dispatcher.Invoke(static () => { }, DispatcherPriority.ApplicationIdle); /// /// Waits for a condition without getting in the way of the timers. /// /// /// Waiting is allowed from any thread, including the interface thread /// itself: there a plain wait would stop the message queue, and with it /// everything being waited for. On the interface thread the queue therefore /// keeps being pumped by a nested loop. /// internal static void WaitFor(Func condition, string because, TimeSpan? timeout = null) { DateTime deadline = DateTime.UtcNow + (timeout ?? DefaultTimeout); while (!condition()) { Assert.True(DateTime.UtcNow < deadline, $"Waited in vain: {because}"); Idle(TimeSpan.FromMilliseconds(5)); } } /// /// Waits for the given time while still pumping the queue: that is how /// "nothing happened during this time" is verified. /// internal static void Pause(TimeSpan duration) { Idle(duration); Drain(); } // A wait during which the dispatcher queue gets its chance to run private static void Idle(TimeSpan duration) { if (Dispatcher.CheckAccess()) { var frame = new DispatcherFrame(); var timer = new DispatcherTimer( duration, DispatcherPriority.Background, (_, _) => frame.Continue = false, Dispatcher); try { Dispatcher.PushFrame(frame); } finally { timer.Stop(); } return; } Thread.Sleep(duration); } private static Dispatcher Start() { var ready = new TaskCompletionSource(); var thread = new Thread(() => { Dispatcher dispatcher = Dispatcher.CurrentDispatcher; // The application has no reason to shut down after its windows: // tests open and close them by the dozen var application = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown }; // The shared style dictionary is merged by App.xaml, which the tests // do not have. Without it the settings window still builds, but not // the way the user will see it application.Resources.MergedDictionaries.Add(new ResourceDictionary { Source = new Uri( "pack://application:,,,/CursorLang.Settings;component/Themes/Controls.xaml", UriKind.Absolute), }); ready.SetResult(dispatcher); Dispatcher.Run(); }) { IsBackground = true, Name = "CursorLang.Tests UI", }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); return ready.Task.GetAwaiter().GetResult(); } }