Files
cursor-lang/CursorLang.Tests/EndToEndTests.cs
T
2026-08-12 04:59:22 +05:00

223 lines
7.7 KiB
C#

using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using CursorLang.Services;
namespace CursorLang.Tests;
/// <summary>
/// The application as a whole: the start, the single instance and the exit.
/// </summary>
/// <remarks>
/// The application cannot be built inside the tests — it raises windows and
/// takes the place of the single instance for the whole session. It is
/// therefore started the way the user starts it: as a separate process.
///
/// If the application is already running in this session, the checks skip
/// themselves: meddling with someone else's running instance is not their business.
///
/// They skip themselves where there is no desktop to show a window on either — on a
/// build agent living as a Windows service, for one.
/// </remarks>
public sealed class EndToEndTests
{
private static readonly TimeSpan StartTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan ExitTimeout = TimeSpan.FromSeconds(15);
/// <summary>How long "the application went on working" is worth watching for.</summary>
private static readonly TimeSpan StayTimeout = TimeSpan.FromSeconds(3);
[Fact]
public void The_application_starts_and_shows_the_settings_window()
{
using Launch launch = Launch.Start();
Assert.NotEqual(IntPtr.Zero, launch.WaitForWindow());
Assert.False(launch.Process.HasExited);
}
/// <summary>
/// Started by Windows itself, the application goes straight to the tray: the
/// user asked for it to be there, not for a window to greet them.
/// </summary>
[Fact]
public void A_launch_by_Windows_shows_no_window()
{
using Launch launch = Launch.Start(StartupLaunch.Argument);
// Nothing is expected to appear, so the wait is for the whole time
Assert.False(launch.Process.WaitForExit(StayTimeout), "the application ended by itself");
launch.Process.Refresh();
Assert.Equal(IntPtr.Zero, launch.Process.MainWindowHandle);
}
// A second run raises no second window but shows the window of the running one
[Fact]
public void The_second_run_ends_by_itself()
{
using Launch launch = Launch.Start();
launch.WaitForWindow();
using Process second = Launch.StartProcess();
Assert.True(second.WaitForExit(ExitTimeout), "the second run did not end by itself");
Assert.Equal(0, second.ExitCode);
// And the first one keeps running
Assert.False(launch.Process.HasExited);
}
// The way out of the application is the tray menu alone: the close button of
// the window merely puts the window away
[Fact]
public void Closing_the_window_leaves_the_application_in_the_tray()
{
using Launch launch = Launch.Start();
launch.WaitForWindow();
Assert.True(launch.Process.CloseMainWindow(), "the window did not accept the request to close");
Assert.False(launch.Process.WaitForExit(StayTimeout), "the application ended together with its window");
launch.Process.Refresh();
Assert.Equal(IntPtr.Zero, launch.Process.MainWindowHandle);
}
/// <summary>A started application that shuts down together with the check.</summary>
private sealed class Launch : IDisposable
{
private const int UOI_NAME = 2;
private const string InteractiveWindowStation = "WinSta0";
[DllImport("user32.dll")]
private static extern IntPtr GetProcessWindowStation();
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern bool GetUserObjectInformation(IntPtr hObj, int nIndex,
StringBuilder pvInfo, int nLength, out int lpnLengthNeeded);
private Launch(Process process) => Process = process;
internal Process Process { get; }
/// <summary>Starts the application first — making sure the place is free.</summary>
internal static Launch Start(params string[] arguments)
{
if (!HasInteractiveDesktop())
{
Assert.Skip("There is no interactive desktop here — the application has nowhere to show its window");
}
if (Process.GetProcessesByName("CursorLang").Length > 0)
{
Assert.Skip("The application is already running — this check keeps out of someone else's run");
}
return new Launch(StartProcess(arguments));
}
/// <summary>Starts the application the way the user — or Windows — does.</summary>
internal static Process StartProcess(params string[] arguments)
{
string path = ExecutablePath();
if (!File.Exists(path))
{
Assert.Skip($"The application is not built: {path}");
}
var start = new ProcessStartInfo(path) { UseShellExecute = true };
foreach (string argument in arguments)
{
start.ArgumentList.Add(argument);
}
return Process.Start(start)!;
}
/// <summary>
/// Waits for the settings window: by the time it appears the application
/// has raised its whole cast.
/// </summary>
internal IntPtr WaitForWindow()
{
DateTime deadline = DateTime.UtcNow + StartTimeout;
while (DateTime.UtcNow < deadline)
{
Process.Refresh();
if (Process.HasExited)
{
Assert.Fail($"The application exited while starting with code {Process.ExitCode}");
}
if (Process.MainWindowHandle != IntPtr.Zero)
{
return Process.MainWindowHandle;
}
Thread.Sleep(100);
}
Assert.Fail("The settings window never appeared");
return IntPtr.Zero;
}
/// <summary>
/// Whether there is a desktop here to show a window on. A service gets a
/// window station of its own — "Service-0x0-3e7$" and the like: a window can
/// be created there, yet nothing shows it. Only "WinSta0" is the interactive one.
/// </summary>
private static bool HasInteractiveDesktop()
{
IntPtr station = GetProcessWindowStation();
if (station == IntPtr.Zero)
{
return false;
}
var name = new StringBuilder(256);
return GetUserObjectInformation(station, UOI_NAME, name, name.Capacity * sizeof(char), out _)
&& name.ToString().Equals(InteractiveWindowStation, StringComparison.OrdinalIgnoreCase);
}
private static string ExecutablePath()
{
string configured = Assembly.GetExecutingAssembly()
.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(attribute => attribute.Key == "CursorLangExecutable")
.Value!;
return Path.GetFullPath(configured);
}
public void Dispose()
{
try
{
if (!Process.HasExited)
{
// Asking the window to close would only put it away into the
// tray, and the exit lives in a menu no test can reach. The
// settings are saved as they change, so nothing is lost here
Process.Kill(entireProcessTree: true);
Process.WaitForExit(ExitTimeout);
}
}
catch (InvalidOperationException)
{
// The process has already ended on its own
}
finally
{
Process.Dispose();
}
}
}
}