180 lines
5.6 KiB
C#
180 lines
5.6 KiB
C#
using System.Windows;
|
|
using System.Windows.Threading;
|
|
|
|
namespace CursorLang.Settings.Tests.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// The user interface thread for the tests.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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 <see cref="Application"/>, 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 <see cref="WaitFor"/>.
|
|
/// </remarks>
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Runs an action on the interface thread and waits for it to finish.</summary>
|
|
internal static void Run(Action action) => Dispatcher.Invoke(action);
|
|
|
|
/// <summary>The same for an action that returns a result.</summary>
|
|
internal static TResult Run<TResult>(Func<TResult> action) => Dispatcher.Invoke(action);
|
|
|
|
/// <summary>
|
|
/// Runs an action on a separate STA thread and waits for it to finish.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits until the dispatcher queue drains: calls deferred through
|
|
/// <c>BeginInvoke</c> have run by that time.
|
|
/// </summary>
|
|
internal static void Drain() =>
|
|
Dispatcher.Invoke(static () => { }, DispatcherPriority.ApplicationIdle);
|
|
|
|
/// <summary>
|
|
/// Waits for a condition without getting in the way of the timers.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
internal static void WaitFor(Func<bool> 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));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for the given time while still pumping the queue: that is how
|
|
/// "nothing happened during this time" is verified.
|
|
/// </summary>
|
|
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<Dispatcher>();
|
|
|
|
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();
|
|
}
|
|
}
|