lightweight variant (#1)

Reviewed-on: #1
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-08-12 13:37:30 +00:00
committed by alex
parent a0d3098fe4
commit 55ac8e6556
147 changed files with 4055 additions and 2349 deletions
@@ -0,0 +1,179 @@
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();
}
}