using System.Diagnostics; using System.IO; using System.Reflection; namespace CursorLang.Tests; /// /// The application as a whole: the start, the single instance and the exit. /// /// /// 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. /// public sealed class EndToEndTests { private static readonly TimeSpan StartTimeout = TimeSpan.FromSeconds(30); private static readonly TimeSpan ExitTimeout = TimeSpan.FromSeconds(15); [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); } // 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); } [Fact] public void Closing_the_window_ends_the_application() { using Launch launch = Launch.Start(); launch.WaitForWindow(); Assert.True(launch.Process.CloseMainWindow(), "the window did not accept the request to close"); Assert.True(launch.Process.WaitForExit(ExitTimeout), "the application did not end after the window closed"); Assert.Equal(0, launch.Process.ExitCode); } /// A started application that shuts down together with the check. private sealed class Launch : IDisposable { private Launch(Process process) => Process = process; internal Process Process { get; } /// Starts the application first — making sure the place is free. internal static Launch Start() { 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()); } /// Starts the application the way the user does. internal static Process StartProcess() { string path = ExecutablePath(); if (!File.Exists(path)) { Assert.Skip($"The application is not built: {path}"); } return Process.Start(new ProcessStartInfo(path) { UseShellExecute = true })!; } /// /// Waits for the settings window: by the time it appears the application /// has raised its whole cast. /// 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; } private static string ExecutablePath() { string configured = Assembly.GetExecutingAssembly() .GetCustomAttributes() .Single(attribute => attribute.Key == "CursorLangExecutable") .Value!; return Path.GetFullPath(configured); } public void Dispose() { try { if (!Process.HasExited) { // The polite way first — that way the app gets to save its settings if (!Process.CloseMainWindow() || !Process.WaitForExit(ExitTimeout)) { Process.Kill(entireProcessTree: true); Process.WaitForExit(ExitTimeout); } } } catch (InvalidOperationException) { // The process has already ended on its own } finally { Process.Dispose(); } } } }